Vuex: Wait for websocket response before dispatching action - javascript

So this is the scenario / premises:
In order to populate a chat queue in real time I need to open a connection to a websocket, send a message and then set the data to a websocket store. This store will basically manage all the websocket state.
Before populating the chat queue there's two parameters I need: a shiftId coming from one http API request and a connectionId coming from the websocket. Using those two parameters I finally can subscribe to a third http API and start receiving messages to populate the chat queue.
The problem is that due to the async behaviour of the websocket (or that's what I think, please feel to correct me if I'm wrong) I always get an empty "connectionId" when trying to make the put to that "subscription" API. I have tried with async/await and promises but nothing seems to work. I'm pretty new to async/await and websockets with Vuex so pretty sure I'm doing something wrong.
This is the user vuex module where I do all the login/token operations and dispatch a "updateEventsSubscription" action from the shift vuex module. In order for the "updateEventsSubscription" action to work I need to get the response from the "processWebsocket" action (to get the connectionId parameter) and from the "startShift" action (to get the shiftId parameter) coming from the shifts vuex module:
import UserService from '#/services/UserService.js'
import TokenService from '#/services/TokenService.js'
import router from '#/router'
export const namespaced = true
export const state = {
accessToken: '',
errorMessage: '',
errorState: false,
userEmail: localStorage.getItem('userEmail'),
userPassword: localStorage.getItem('userPassword'),
}
export const mutations = {
SET_TOKEN(state, accessToken) {
state.accessToken = accessToken
TokenService.saveToken(accessToken)
},
SET_USER(state, authUserJson) {
state.userEmail = authUserJson.email
state.userPassword = authUserJson.password
localStorage.setItem('userPassword', authUserJson.password)
localStorage.setItem('userEmail', authUserJson.email)
},
SET_ERROR(state, error) {
state.errorState = true
state.errorMessage = error.data.error_description
},
CLOSE_NOTIFICATION(state, newErrorState) {
state.errorState = newErrorState
},
}
export const actions = {
signIn({ commit, dispatch, rootState }, authUserJson) {
return UserService.authUser(authUserJson)
.then((result) => {
commit('SET_USER', authUserJson)
commit('SET_TOKEN', result.data.access_token)
dispatch('token/decodeToken', result.data.access_token, {
root: true,
})
dispatch(
'shifts/updateEventsSubscription',
rootState.token.agentId,
{
root: true,
}
)
router.push('/support')
})
.catch((error) => {
console.log(error)
if (error.response.status === 400) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
commit('SET_ERROR', error.response)
} else {
console.log(error.response)
}
})
},
signOut({ commit }) {
commit('SET_TOKEN', null)
commit('SET_USER', {})
localStorage.removeItem('userPassword')
localStorage.removeItem('userEmail')
TokenService.removeToken()
router.push('/')
},
closeNotification({ commit }, newErrorState) {
commit('CLOSE_NOTIFICATION', newErrorState)
},
}
export const getters = {
getToken: (state) => {
return state.accessToken
},
errorState: (state) => {
return state.errorState
},
errorMessage: (state) => {
return state.errorMessage
},
isAuthenticated: (state) => {
return state.accessToken
},
userEmail: (state) => {
return state.userEmail
},
userPassword: (state) => {
return state.userPassword
},
}
This is websocket store: I pass the connectionId to the state in order to be able to use it in another vuex action to subscribe for new chats:
export const namespaced = true
export const state = {
connected: false,
error: null,
connectionId: '',
statusCode: '',
incomingChatInfo: [],
remoteMessage: [],
messageType: '',
ws: null,
}
export const actions = {
processWebsocket({ commit }) {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
},
}
export const mutations = {
SET_REMOTE_DATA(state, remoteData) {
const wsData = JSON.parse(remoteData.data)
if (wsData.connectionId) {
state.connectionId = wsData.connectionId
console.log(`Retrieving Connection ID ${state.connectionId}`)
} else {
console.log(`We got chats !!`)
state.messageType = wsData.type
state.incomingChatInfo = wsData.documents
}
},
SET_CONNECTION(state, message) {
if (message == 'open') {
state.connected = true
} else state.connected = false
},
SET_ERROR(state, error) {
state.error = error
},
}
And finally this is the shift store (where the problem is), as you can see I have a startShift action (everything works fine with it) and then the "updateEventsSubscription" where I'm trying to wait for the response from the "startShift" action and the "processWebsocket" action. Debugging the app I realize that everything works fine with the startShift action but the websocket action sends the response after the "updateEventsSubscription" needs it causing an error when I try to make a put to that API (because it needs the connectionId parameter coming from the state of the websocket).
import ShiftService from '#/services/ShiftService.js'
export const namespaced = true
export const state = {
connectionId: '',
shiftId: '',
agentShiftInfo: '{}',
}
export const actions = {
startShift({ commit }, agentId) {
return ShiftService.startShift(agentId)
.then((response) => {
if (response.status === 200) {
commit('START_SHIFT', response.data.aggregateId)
}
})
.catch((error) => {
console.log(error)
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
async updateEventsSubscription({ dispatch, commit, rootState }, agentId) {
await dispatch('startShift', agentId)
const shiftId = state.shiftId
await dispatch('websocket/processWebsocket', null, { root: true })
let agentShiftInfo = {
aggregateId: state.shiftId,
connectionId: rootState.websocket.connectionId,
}
console.log(agentShiftInfo)
return ShiftService.updateEventsSubscription(shiftId, agentShiftInfo)
.then((response) => {
commit('UPDATE_EVENTS_SUBSCRIPTION', response.data)
})
.catch((error) => {
if (error.response.status === 401) {
console.log('Error in Response')
}
})
},
}
export const mutations = {
START_SHIFT(state, shiftId) {
state.shiftId = shiftId
console.log(`Retrieving Shift ID: ${state.shiftId}`)
},
UPDATE_EVENTS_SUBSCRIPTION(state, agentShiftInfo) {
state.agentShiftInfo = agentShiftInfo
},
}

You should convert your WebSocket action into a promise that resolves when WebSocket is connected.:
export const actions = {
processWebsocket({ commit }) {
return new Promise(resolve=> {
const v = this
this.ws = new WebSocket('mywebsocket')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
resolve();
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
});
},
}

So I realized that I have to resolve the promise on the this.ws.message instead. By doing that all my data is populated accordingly, there's still sync issues (I can't feed the websocket state at the moment because due to its async behaviour the state is not there yet when other components try to use it via: rootGetters.websocket.incomingChats for example) but I guess that's part of another question. Here's the final version of the module action:
export const actions = {
processWebsocket({ commit }) {
return new Promise((resolve) => {
const v = this
this.ws = new WebSocket('wss://ws.rubiko.io')
this.ws.onopen = function (event) {
commit('SET_CONNECTION', event.type)
v.ws.send('message')
}
this.ws.onmessage = function (event) {
commit('SET_REMOTE_DATA', event)
resolve(event)
}
this.ws.onerror = function (event) {
console.log('webSocket: on error: ', event)
}
this.ws.onclose = function (event) {
console.log('webSocket: on close: ', event)
commit('SET_CONNECTION')
ws = null
setTimeout(startWebsocket, 5000)
}
})
},
}
Anyways, thanks #Eldar you were in the right path.

Related

Not able to get the id of the generated firebase document

I'm trying to get the id of the generated firebase document, and I'm using addDoc to create a new doc.
I'm generating a new document on button click and that button calls the initializeCodeEditor function.
Anyone please help me with this!
Button Code:
import { useNavigate } from "react-router-dom"
import { useAuthContext } from "../../hooks/useAuthContext"
import { useFirestore } from "../../hooks/useFirestore"
import Button from "./Button"
const StartCodingButton = ({ document, setIsOpen }) => {
const { user } = useAuthContext()
const { addDocument, response } = useFirestore("solutions")
const navigate = useNavigate()
const initializeCodeEditor = async () => {
await addDocument({
...document,
author: user.name,
userID: user.uid,
})
if (!response.error) {
console.log(response.document) // null
const id = response?.document?.id; // undefined
navigate(`/solution/${id}`, { state: true })
}
}
return (
<Button
className="font-medium"
variant="primary"
size="medium"
onClick={initializeCodeEditor}
loading={response.isPending}
>
Start coding online
</Button>
)
}
export default StartCodingButton
addDocument code
import { useReducer } from "react"
import {
addDoc,
collection,
doc,
Timestamp,
} from "firebase/firestore"
import { db } from "../firebase/config"
import { firestoreReducer } from "../reducers/firestoreReducer"
const initialState = {
document: null,
isPending: false,
error: null,
success: null,
}
export const useFirestore = (c) => {
const [response, dispatch] = useReducer(firestoreReducer, initialState)
// add a document
const addDocument = async (doc) => {
dispatch({ type: "IS_PENDING" })
try {
const createdAt = Timestamp.now()
const addedDocument = await addDoc(collection(db, c), {
...doc,
createdAt,
})
dispatch({ type: "ADDED_DOCUMENT", payload: addedDocument })
} catch (error) {
dispatch({ type: "ERROR", payload: error.message })
}
}
return {
addDocument,
response,
}
}
firestoreReducer
export const firestoreReducer = (state, action) => {
switch (action.type) {
case "IS_PENDING":
return { isPending: true, document: null, success: false, error: null }
case "ADDED_DOCUMENT":
return { isPending: false, document: action.payload, success: true, error: null }
}
throw Error("Unknown action: " + action.type)
}
I have recreated this issue and found out this is happening because the response object in the useFirestore hook is not being updated until the next render cycle.
In order to get the updated response object, you can use the useEffect hook to trigger an update to the component whenever the response object changes.
So I recommend you to call initializeCodeEditor and make your app wait until response object change I used useEffect here
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
})
//skip following if block it's just for understanding
if (!response.error) {
console.log(response.document) // will obviously be null here as at first it is set null
const id = response?.document?.id; // will obviously be undefined
navigate(`/solution/${id}`, { state: true })
}
}
useEffect(() => {
if (!response.error) {
setId(response?.document?.id);
console.log("From App.js useEffect: " + response?.document?.id); // getting the document id here too
}
}, [response])
//and in firestoreReducer
case "ADDED_DOCUMENT":{
console.log("from Reducer: " + action.payload.id); //getting the document id here
return { isPending: false, document: action.payload, success: true, error: null }
}
OR you can use callback also without introducing useEffect like this:
const initializeCodeEditor = async () => {
await addDocument({
author: user.name,
userID: user.uid,
}, (response) => {
console.log("From App: " + response?.document?.id); //Will run as callback
if (!response.error) {
setId(response?.document?.id);
}
})
}
This way, the callback function will be called after the addDocument function has completed and the response object will have the updated document id.

Firebase updating Documents delayed by one onClick event

Using react.js & firebase
The code below represents a simple button which increases/decreases +1/-1 whenever its clicked. It also updates one of the documents on the backend (using firebase). Everything seems to work fine on the surface but not on firebase. When you click on the button, it'll show +1 on the UI and console.log but not on firebase. In other words when plusCount state is at 0, it shows +1 on firebase and when plusCount state is at +1, it shows 0 on firebase. How can I fix this to make sure it shows the same number on the frontend and the backend? I also added the useFirestore hook component below, there may be a mistake that I'm unaware of in there somewhere.
Thank you for any help.
Button component:
import React, { useState } from 'react';
import { useFirestore } from "../../hooks/useFirestore"
export default function Testing({ doc }) {
const { updateDocument } = useFirestore('projects')
const [plusActive, setPlusActive] = useState(false)
const [plusCount, setPlusCount] = useState(0)
function p() {
setPlusActive(prevState => !prevState);
plusActive ? setPlusCount(plusCount - 1) : setPlusCount(plusCount + 1)
}
const handlePlus = (e) => {
e.preventDefault();
p();
updateDocument(doc.id, {
votes: plusCount
})
}
console.log(plusCount)
return (
<div>
<button onClick={handlePlus}>like | {plusCount}</button>
</div>
)
}
useFirestore hook component:
import { projectFirestore, timestamp } from "../firebase/config"
let initialState = {
document: null,
isPending: false,
error: null,
success: null,
}
const firestoreReducer = (state, action) => {
switch (action.type) {
case 'IS_PENDING':
return { isPending: true, document: null, success: false, error: null }
case 'ADDED_DOCUMENT':
return { isPending: false, document: action.payload, success: true, error: null }
case 'DELETED_DOCUMENT':
return { isPending: false, document: null, success: true, error: null }
case 'ERROR':
return { isPending: false, document: null, success: false, error: action.payload }
case "UPDATED_DOCUMENT":
return { isPending: false, document: action.payload, success: true, error: null }
default:
return state
}
}
export const useFirestore = (collection) => {
const [response, dispatch] = useReducer(firestoreReducer, initialState)
const [isCancelled, setIsCancelled] = useState(false)
// collection ref
const ref = projectFirestore.collection(collection)
// only dispatch if not cancelled
const dispatchIfNotCancelled = (action) => {
if (!isCancelled) {
dispatch(action)
}
}
// add a document
const addDocument = async (doc) => {
dispatch({ type: 'IS_PENDING' })
try {
const createdAt = timestamp.fromDate(new Date())
const addedDocument = await ref.add({ ...doc, createdAt })
dispatchIfNotCancelled({ type: 'ADDED_DOCUMENT', payload: addedDocument })
}
catch (err) {
dispatchIfNotCancelled({ type: 'ERROR', payload: err.message })
}
}
// delete a document
const deleteDocument = async (id) => {
dispatch({ type: 'IS_PENDING' })
try {
await ref.doc(id).delete()
dispatchIfNotCancelled({ type: 'DELETED_DOCUMENT' })
}
catch (err) {
dispatchIfNotCancelled({ type: 'ERROR', payload: 'could not delete' })
}
}
// update a document
const updateDocument = async (id, updates) => {
dispatch({ type: "IS_PENDING" })
try {
const updatedDocument = await ref.doc(id).update(updates)
dispatchIfNotCancelled({ type: "UPDATED_DOCUMENT", payload: updatedDocument })
return updatedDocument
}
catch (error) {
dispatchIfNotCancelled({ type: "ERROR", payload: error })
return null
}
}
useEffect(() => {
return () => setIsCancelled(true)
}, [])
return { addDocument, deleteDocument, updateDocument, response }
}```
For your use-case, you should useEffect() to listen the changes for plusCount. See code below:
useEffect(() => {
updateDocument('test', {
votes: plusCount
})
}, [plusCount]);
const handlePlus = (e) => {
e.preventDefault();
setPlusActive(prevState => !prevState);
plusActive ? setPlusCount(plusCount - 1) : setPlusCount(plusCount + 1)
}
Everytime you click the button it will listen to the changes of plusCount which then the updateDocument will also be triggered together with the updated state. See below screenshot for the result:
As you can see, the frontend and backend is now aligned.
You can find more information by checking out this documentation.

Cannot read property 'conversationId' of undefined while using a reducer function

Just to make it clear router uses the code below and my messages.js are inside api folder....
router.use("/messages", require("./messages"));
so my api call is correct.
Backend for posting the message.... I know conversationId will be null if no conversation exists but... I am trying to send message where conversation exists already and still I am getting cannot read the conversationId of undefined....
// expects {recipientId, text, conversationId } in body
// (conversationId will be null if no conversation exists yet)
router.post("/", async (req, res, next) => {
try {
if (!req.user) {
return res.sendStatus(401);
}
const senderId = req.user.id;
const { recipientId, text, conversationId, sender } = req.body;
// if we already know conversation id, we can save time and just add it to message and return
if (conversationId) {
const message = await Message.create({ senderId, text, conversationId });
return res.json({ message, sender });
}
// if we don't have conversation id, find a conversation to make sure it doesn't already exist
let conversation = await Conversation.findConversation(
senderId,
recipientId
);
if (!conversation) {
// create conversation
conversation = await Conversation.create({
user1Id: senderId,
user2Id: recipientId,
});
if (onlineUsers.includes(sender.id)) {
sender.online = true;
}
}
const message = await Message.create({
senderId,
text,
conversationId: conversation.id,
});
res.json({ message, sender });
} catch (error) {
next(error);
}
});
module.exports = router;
This is the frontend that posts the data to the backend....
const saveMessage = async (body) => {
const { data } = await axios.post("/api/messages", body);
return data;
};
Okay so here is detail information on how I am dispatching it.
class Input extends Component {
constructor(props) {
super(props);
this.state = {
text: "",
};
}
handleChange = (event) => {
this.setState({
text: event.target.value,
});
};
handleSubmit = async (event) => {
event.preventDefault();
// add sender user info if posting to a brand new convo,
// so that the other user will have access to username, profile pic, etc.
const reqBody = {
text: event.target.text.value,
recipientId: this.props.otherUser.id,
conversationId: this.props.conversationId,
sender: this.props.conversationId ? null : this.props.user,
};
await this.props.postMessage(reqBody);
this.setState({
text: "",
});
};
render() {
const { classes } = this.props;
return (
<form className={classes.root} onSubmit={this.handleSubmit}>
<FormControl fullWidth hiddenLabel>
<FilledInput
classes={{ root: classes.input }}
disableUnderline
placeholder="Type something..."
value={this.state.text}
name="text"
onChange={this.handleChange}
/>
</FormControl>
</form>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(withStyles(styles)(Input));
const mapDispatchToProps = (dispatch) => {
return {
postMessage: (message) => {
dispatch(postMessage(message));
},
};
};
// message format to send: {recipientId, text, conversationId}
// conversationId will be set to null if its a brand new conversation
export const postMessage = (body) => (dispatch) => {
try {
const data = saveMessage(body);
if (!body.conversationId) {
dispatch(addConversation(body.recipientId, data.message));
} else {
dispatch(setNewMessage(data.message));
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
So I have attached what I want to do here now....
But I am still getting the problem....
// CONVERSATIONS THUNK CREATORS, this is how I am getting data from the backend
export const fetchConversations = () => async (dispatch) => {
try {
const { data } = await axios.get("/api/conversations");
dispatch(gotConversations(data));
} catch (error) {
console.error(error);
}
};
export const setNewMessage = (message, sender) => {
return {
type: SET_MESSAGE,
payload: { message, sender: sender || null },
};
};
// REDUCER
const reducer = (state = [], action) => {
switch (action.type) {
case GET_CONVERSATIONS:
return action.conversations;
case SET_MESSAGE:
return addMessageToStore(state, action.payload);
case ADD_CONVERSATION:
return addNewConvoToStore(
state,
action.payload.recipientId,
action.payload.newMessage
);
default:
return state;
}
};
I am getting an error saying Cannot read property 'conversationId' of undefined while using a reducer function... Should I give the setintial value of the message to empty?
export const addMessageToStore = (state, payload) => {
const { message, sender } = payload;
// if sender isn't null, that means the message needs to be put in a brand new convo
if (sender !== null) {
const newConvo = {
id: message.conversationId,
otherUser: sender,
messages: [message],
};
newConvo.latestMessageText = message.text;
return [newConvo, ...state];
}
return state.map((convo) => {
if (convo.id === message.conversationId) {
const convoCopy = { ...convo };
convoCopy.messages.push(message);
convoCopy.latestMessageText = message.text;
return convoCopy;
} else {
return convo;
}
});
};
Issue
The saveMessage function is declared async
const saveMessage = async (body) => {
const { data } = await axios.post("/api/messages", body);
return data;
};
but the postMessage action creator isn't async so it doesn't wait for the implicitly returned Promise to resolve before continuing on and dispatching to the store. This means that data.message is undefined since a Promise object doesn't have this as a property.
export const postMessage = (body) => (dispatch) => {
try {
const data = saveMessage(body); // <-- no waiting
if (!body.conversationId) {
dispatch(addConversation(body.recipientId, data.message));
} else {
dispatch(setNewMessage(data.message));
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};
Solution
Declare postMessage async as well and await the data response value.
export const postMessage = (body) => async (dispatch) => {
try {
const data = await saveMessage(body); // <-- await response
if (!body.conversationId) {
dispatch(addConversation(body.recipientId, data.message));
} else {
dispatch(setNewMessage(data.message));
}
sendMessage(data, body);
} catch (error) {
console.error(error);
}
};

Way to make inheritance in Vuex modules

Im building my app with VueJS and Vuex and I'm facing the issue when I have Multiple modules using the same data fields. Its about API configuration like dat.
getUsers ({ state, commit }) {
axios.get(urls.API_USER_URL).then( response => {
let data = response.data;
parseApi(state, data, 'user');
}).catch( err => {
console.log('getUser error: ', err);
})
},
And another function in other Modules is like
getPosts ({ state, commit }) {
axios.get(urls.API_POST_URL).then( response => {
let data = response.data;
parseApi(state, data, 'posts');
}).catch( err => {
console.log('getUser error: ', err);
})
},
I would like to know if I can just inheritence my Module and add additional datafields / functions in there?
My every module would have message and status field which I getting in response of my API.
export default {
state : {
message : "",
status : 0
},
parseApi: function(state, data, property) {
if (data.hasOwnProperty('message')) {
state.message = data.message;
}
if (data.hasOwnProperty('status')) {
state.status = data.status;
}
if (data.hasOwnProperty(property)) {
state[property] = data[property];
}
}
}
It would be something like that.
Is there a way to write this code once and have it in every module Im using?
EDITED:
I even cant get this apiParse function in there, I need to make muttation for those fields. But repeting it all time is pointless... Any advices?
I put my reusable vuex code in small classes. E.g.
crud.js
export default class {
constructor ( endpoint ) {
this.state = {
endpoint: endpoint,
meta: {},
status: null,
known: [],
currentId: null,
};
this.getters = {
id: state => id => state.known.find( o => o.id === id )
};
this.actions = {
async store( context, payload ) {
*(call to API)*
},
async update( context, payload ) {
*(call to API)*
},
*...etc*
};
this.mutations = {
STORED(state, item) {
state.known.push(item);
},
*...etc*
};
}
}
Then I can use it in all of my modules:
user.module.js
import Crud from '/crud';
var crud = new Crud('/api/users');
const state = {
...crud.state,
};
const getters = {
...crud.getters,
};
const actions = {
...crud.actions,
};
const mutations = {
...crud.mutations,
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};
Developing a little bit more Erin's response, you can define a base class with common features like this:
export default class BaseModule {
protected state() {
return {
isLoading: false,
};
};
protected getters() {
return {
isLoading(s) {
return s.isLoading;
},
};
};
protected actions() {
return {};
};
protected mutations() {
return {
[START_TRANSACTION]: (s) => {
s.isLoading = true;
},
[END_TRANSACTION]: (s) => {
s.isLoading = false;
},
};
}
protected modules() {
return {};
};
public getModule = () => {
return {
namespaced: true,
state: this.state(),
getters: this.getters(),
actions: this.actions(),
mutations: this.mutations(),
modules: this.modules(),
};
}
}
You can now extend/override only the parts you need in derived classes, with class inheritance; for example, if you need to extend the modules...:
import BaseModule from './BaseModule';
import rowDensity from '#/store/modules/reusable/rowDensity';
export default class ItemListModule extends BaseModule {
protected modules() {
return {
...super.modules(),
rowDensity,
};
};
}
Finally, to use them as modules in the store, you can instantiate them and call .getModule():
import Vue from 'vue';
import Vuex from 'vuex';
import ItemListModule from './modules/ItemListModule';
Vue.use(Vuex);
const debug = process.env.NODE_ENV !== 'production';
export const MODULE_NAMESPACES = {
List: 'list',
};
export default new Vuex.Store({
modules: {
[MODULE_NAMESPACES.List]: new ItemListModule().getModule(),
},
strict: debug,
});
I figured out some inheritance with the state fields according to:
https://vuex.vuejs.org/en/modules.html#namespacing
export default {
namespaced: true,
state,
getters,
actions,
mutations,
modules : {
apiResponses
}
}
I exported apiResponses module after the module user with namespaced and next i did the same thing with posts.
The namespaces inherited those message / status states and their mutations and which i just called in my user and post module. Now they are working corectly.
My message muttation form apiResponses:
[types.SET_MESSAGE] (state, message) {
state.message = message;
},
Works inside actions of my user modules
if (data.hasOwnProperty('message')) {
commit(types.SET_MESSAGE, data.message);
}
Then in my commponent I just call.
computed: {
...mapGetters({
user : 'user/user',
userMessage : 'user/message',
post: 'post/monitoring',
postMessage : 'post/message',
}),
},
EDITED
The last part of my issue is like that.
I got action inside apiResponse Module
let actions = {
getResponseParsed({commit}, payload) {
console.log(payload)
if (payload.data.hasOwnProperty('message')) {
commit(types.SET_MESSAGE, payload.data.message);
}
if (payload.data.hasOwnProperty('status')) {
commit(types.SET_STATUS, payload.data.status);
}
if (payload.data.hasOwnProperty(payload.property)) {
commit(payload.mutation, payload.data[payload.property]);
}
}
}
And then inside my user and other module i called it like:
getUser ({ state, commit, dispatch }) {
axios.get(urls.API_GET_USER_URL).then( response => {
let data = response.data;
dispatch('getResponseParsed', {
data : data,
mutation : types.SET_USER,
property : 'user'
});
});
},
And the last thing, we need to make this new module reusable to according to docs we need to create it like a components.
export default {
state() {
return {
message : '',
status : 0,
}
},
getters,
mutations,
actions
}
With the state as function :)
Hope somone else got same issue :D
here is what I've done:
first of all, I created a mainApi.js whose duty is to just make connection with apis
mainApi.js
import axios from "#/plugins/axios";
export default {
get(url ,id){
return axios.get(`/${url}/${id}`);
},
getAll(url, filter) {
return axios.get(`/${url}`, {params: {...filter}});
},
create(url ,teBeCreated){
return axios.post(`/${url}`, teBeCreated);
},
update(url ,toBeUpdated){
return axios.put(`/${url}/${toBeUpdated.oid}`, toBeUpdated);
},
delete(url ,id){
return axios.delete(`/${url}/${id}`);
},
}
second: I wrote a base class to define needed functions to store data. then this class can be inherited by other store modules.
gate.js
import mainApi from '#/api/main'
import store from '#/store'
export default class {
constructor() {
this.state = {
view: null,
list: [],
};
this.getters = {
view: (state) => state.view,
list: (state) => state.list,
}
this.mutations = {
SET_VIEW(state, payload) {
state.view = payload;
},
SET_LIST(state, payload) {
state.list = payload;
},
UN_SET_VIEW(state) {
state.view = null;
},
UN_SET_LIST(state) {
state.list = [];
},
}
this.actions = {
get({ commit }, { url, id }) {
return new Promise((resolve, reject) => {
mainApi.get(url, id)
.then(response => {
commit('SET_VIEW', response.data.data);
resolve(response)
})
.catch(error => {
console.log("error in get method in gate store: ", error);
commit('UN_SET_VIEW');
reject(error)
})
});
},
getAll({ commit }, { url, filter }) {
return new Promise((resolve, reject) => {
mainApi.getAll(url, filter)
.then(response => {
commit('SET_LIST', response.data.data);
resolve(response)
})
.catch(error => {
console.log("error in getAll method in gate store: ", error);
commit('UN_SET_LIST');
reject(error)
})
});
},
create({ commit }, { url, params }) {
return new Promise((resolve, reject) => {
mainApi.create(url, params)
.then(response => {
resolve(response)
})
.catch(error => {
console.log("error in create method in gate store: ", error);
reject(error)
});
});
},
update({ commit }, { url, params }) {
return new Promise((resolve, reject) => {
mainApi.update(url, params)
.then(response => {
resolve(response)
})
.catch(error => {
console.log("error in update method in gate store: ", error);
reject(error)
})
})
},
delete({ commit }, { url, id }) {
return new Promise((resolve, reject) => {
mainApi.delete(url, id)
.then(response => {
resolve(response);
})
.catch(error => {
console.log("error in delete method in gate store: ", error);
reject(error)
})
});
},
}
}
third: now, we can define as many separate store modules as we need. as you can see below, in each module we just need to get the data retrieved from views and pass them to mainApi (gate.js base class's functions and methods are all part of our modules) and manipulate with received data.
someStore.js
import Gate from '#/store/modules/gate'
let gate = new Gate();
const url = 'customUrl'
const gateStates = { ...gate.state }
const gateGetters = { ...gate.getters }
const gateMutations = { ...gate.mutations }
const state = {
...gateStates,
};
const getters = {
...gateGetters,
};
const mutations = {
...gateMutations,
};
const actions = {
get: ({ commit }, id) => gate.actions.get({ commit }, { url, id }),
getAll: ({ commit }) => gate.actions.getAll({ commit }, {url, filter: {}}),
create: ({ commit }, params) => gate.actions.create({ commit }, { url, params }),
update: ({ commit }, params) => gate.actions.update({ commit }, { url, params }),
delete: ({ commit }, id) => gate.actions.delete({ commit }, { url, id })
};
export default {
namespaced: true,
state,
getters,
actions,
mutations
};
finally we should import our modules and define them as "vuex store modules" so:
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import someModule from './modules/someModule'
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {
someModule
},
plugins: {}
})
in this example I used anotherPromise because I needed the server responses directly in my views. if you want to just use responses in your store, there is no need for these Promises and they should be removed as below:
in gate.js
change this
get({ commit }, { url, id }) {
return new Promise((resolve, reject) => {
mainApi.get(url, id)
.then(response => {
commit('SET_VIEW', response.data.data);
resolve(response)
})
.catch(error => {
commit('UN_SET_VIEW');
console.log("error in getOne method in gate store: ", error);
reject(error)
})
});
},
to this
get({ commit }, { url, id }) {
mainApi.get(url, id)
.then(response => {
commit('SET_VIEW', response.data.data);
})
.catch(error => {
commit('UN_SET_VIEW');
console.log("error in getOne method in gate store: ", error);
})
},
in this way, you have list and view parameters in each module and they can be easily called in your views:
someView.vue
created() {
store.dispatch('someModule/get', this.$route.params.id)
}
computed: {
view() {
return store.getters('someModule/view')
}
}
As a personal challenge I wanted to be able to create a pure ES6 class that could express this need (meaning no annotation allowed). I thus created an AbstractModule class defining the high level operations:
export default class AbstractModule {
constructor(namespaced = true) {
this.namespaced = namespaced;
}
_state () {
return {}
}
_mutations () {
return {}
}
_actions () {
return {}
}
_getters () {
return {}
}
static _exportMethodList (instance, methods) {
let result = {};
// Process methods when specified as array
if (Array.isArray(methods)) {
for (let method of methods) {
if (typeof method === 'string') {
result[method] = instance[method].bind(instance);
}
if (typeof method === 'function') {
result[method.name] = method.bind(instance);
}
// else ignore
}
}
// Process methods when specified as plain object
if (typeof methods === "object") {
for (const [name, method] of Object.entries(methods)) {
if (typeof method === 'string') {
result[name] = instance[method].bind(instance);
}
if (typeof method === 'function') {
result[name] = method.bind(instance);
}
}
}
// Process methods when specified as single string
if (typeof methods === 'string') {
result[name] = instance[methods].bind(instance);
}
// Process methods when specified as single callback
if (typeof methods === 'function') {
result[name] = methods.bind(instance);
}
return result;
}
static module() {
let instance = new this();
console.log(instance);
return {
namespaced: instance.namespaced,
state: instance._state(),
mutations: AbstractModule._exportMethodList(instance, instance._mutations()),
actions: AbstractModule._exportMethodList(instance, instance._actions()),
getters: AbstractModule._exportMethodList(instance, instance._getters())
}
}
}
From this I created my own class module by redefining the parent methods I wanted to customize this way:
export default class QuestionModule extends AbstractModule{
constructor(question) {
super();
this.question = question;
}
selectLine (state, line) {
this.question.selectLine(line);
}
unselectLine (state, line) {
this.question.unselectLine(line);
}
submit ({ state, commit, rootState }) {
/** API call */
}
_state () {
return this.question;
}
_mutations () {
return [this.selectLine, this.unselectLine, this.validate];
}
_actions () {
return this.submit;
}
}
Final step is to declare my class module into the Vuex store (through a call to the module static method):
const store = new Vuex.Store({
modules: {
question: QuestionModule.module()
},
strict: process.env.NODE_ENV !== 'production'
});

Testing Async Redux Action Jest

I'm having trouble getting the correct output from an async redux action. I am using Jest, redux-mock-adapter, and thunk as the tools.
According to redux's documentation on testing async thunks (https://redux.js.org/docs/recipes/WritingTests.html#async-action-creators), my tests should be returning an array of two actions. However, my test is only returning the first action, and not the second one that should return on a successful fetch. I think I'm just missing something small here, but it has been bothersome to say the least.
Redux Action
export const getRemoveFileMetrics = cacheKey => dispatch => {
dispatch({ type: IS_FETCHING_DELETE_METRICS });
return axios
.get("GetRemoveFileMetrics", { params: { cacheKey } })
.then(response => dispatch({ type: GET_REMOVE_FILE_METRICS, payload: response.data }))
.catch(err => err);
};
Test
it("getRemoveFileMetrics() should dispatch GET_REMOVE_FILE_METRICS on successful fetch", () => {
const store = mockStore({});
const cacheKey = "abc123doremi";
const removeFileMetrics = {
cacheKey,
duplicateFileCount: 3,
uniqueFileCount: 12,
};
const expectedActions = [
{
type: MOA.IS_FETCHING_DELETE_METRICS,
},
{
type: MOA.GET_REMOVE_FILE_METRICS,
payload: removeFileMetrics,
}
];
mockRequest.onGet(`/GetRemoveFileMetrics?cacheKey=${cacheKey}`).reply(200, removeFileMetrics);
return store.dispatch(MOA.getRemoveFileMetrics(cacheKey)).then(() => {
const returnedActions = store.getActions();
expect(returnedActions).toEqual(expectedActions);
});
});
The Output
Expected value to equal:
[{ "type": "IS_FETCHING_DELETE_METRICS" }, { "payload": { "cacheKey": "abc123doremi", "duplicateFileCount": 3, "uniqueFileCount": 12 }, "type": "GET_REMOVE_FILE_METRICS" }]
Received:
[{ "type": "IS_FETCHING_DELETE_METRICS" }]
I am using jest-fetch-mock and no axios. The following is working for me with the actions. You could refactor to async await as first step. For me it only worked that way.
I am now trying to figure out how to test the side effect (showErrorAlert(jsonResponse);). If I mock out the showErrorAlert implementation at the top of the test file (commented out in my example) then I get the same problem just like you. Actions that uses fetch won't get triggered for some reason.
export const submitTeammateInvitation = (data) => {
const config = {
//.....
};
return async (dispatch) => {
dispatch(submitTeammateInvitationRequest());
try {
const response = await fetch(inviteTeammateEndpoint, config);
const jsonResponse = await response.json();
if (!response.ok) {
showErrorAlert(jsonResponse);
dispatch(submitTeammateInvitationError(jsonResponse));
throw new Error(response.statusText);
}
dispatch(submitTeammateInvitationSuccess());
} catch (error) {
if (process.env.NODE_ENV === 'development') {
console.log('Request failed', error);
}
}
};
};
test
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
// jest.mock('../../../../_helpers/alerts', ()=> ({ showAlertError: jest.fn() }));
const middlewares = [thunk];
const createMockStore = configureMockStore(middlewares);
......
it('dispatches the correct actions on a failed fetch request', () => {
fetch.mockResponse(
JSON.stringify(error),
{ status: 500, statusText: 'Internal Server Error' }
);
const store = createMockStore({});
const expectedActions = [
{
type: 'SUBMIT_TEAMMATE_INVITATION_REQUEST',
},
{
type: 'SUBMIT_TEAMMATE_INVITATION_FAILURE',
payload: { error }
}
];
return store.dispatch(submitTeammateInvitation(data))
.then(() => {
// expect(alerts.showAlertError).toBeCalled();
expect(store.getActions()).toEqual(expectedActions);
});
});

Categories