I am having trouble with my Redux store in cases where I am passing params through a Thunk action. In cases were there is no param, my store is populating correctly. The action is completing successfully and I can see that the data has been returned to the front end by the success / fulfilled message of my action but there is no sign of it going into the store as state.
I had an instance previously where the array list was named incorrectly from the backend however this is not the case this time.
Is there anything that stands out why my store isn't populating with the state data?
action
export const requireUserDiveLogData = createAsyncThunk(
'users/requireData', // action name
// action expects to be called with the name of the field
async (userId) => {
// you need to define a function to fetch the data by field name
const response = await userDiveLogList(userId);
// what we return will be the action payload
return response.data;
},
// only fetch when needed: https://redux-toolkit.js.org/api/createAsyncThunk#canceling-before-execution
{
// _ denotes variables that aren't used - the first argument is the args of the action creator
condition: (_, { getState }) => {
const { users } = getState(); // returns redux state
// check if there is already data by looking at the didLoadData property
if (users.didLoadDiveLogData) {
// return false to cancel execution
return false;
}
}
}
)
reducer
export const userSlice = createSlice({
name: 'users',
initialState: {
userDiveLogList: [],
didLoadDiveLogData: false,
},
reducers: {
[requireUserDiveLogData.pending.type]: (state) => {
state.didLoadDiveLogData = true;
},
[requireUserDiveLogData.fulfilled.type]: (state, action) => {
return {
...state,
...action.payload
}
},
}
})
You should use extraReducers rather than reducers to handle actions produced by createAsyncThunk and createAction functions.
Besides, Redux Toolkit's createReducer and createSlice automatically use Immer internally to let you write simpler immutable update logic using "mutating" syntax. You don't need to do the shallow copy work by yourself.
E.g.
// #ts-nocheck
import {
configureStore,
createAsyncThunk,
createSlice,
} from '#reduxjs/toolkit';
async function userDiveLogList(userId) {
return { data: { userDiveLogList: [1, 2, 3] } };
}
export const requireUserDiveLogData = createAsyncThunk(
'users/requireData',
async (userId) => {
const response = await userDiveLogList(userId);
return response.data;
},
{
condition: (_, { getState }) => {
const { users } = getState();
if (users.didLoadDiveLogData) {
return false;
}
},
}
);
const userSlice = createSlice({
name: 'users',
initialState: {
userDiveLogList: [],
didLoadDiveLogData: false,
},
reducers: {},
extraReducers: (builder) => {
builder
.addCase(requireUserDiveLogData.pending, (state) => {
state.didLoadDiveLogData = true;
})
.addCase(requireUserDiveLogData.fulfilled, (state, action) => {
state.userDiveLogList = action.payload.userDiveLogList;
});
},
});
const store = configureStore({
reducer: {
users: userSlice.reducer,
},
});
store.dispatch(requireUserDiveLogData()).then(() => {
console.log(JSON.stringify(store.getState(), null, 2));
});
Output in the console:
{
"users": {
"userDiveLogList": [
1,
2,
3
],
"didLoadDiveLogData": true
}
}
Related
This is my jsonApiFetchOiAndLTPs reducer
export const jsonApiFetchOiAndLTPs = createAsyncThunk('exchange/jsonApiFetchOiAndLTPs', async (payload, { dispatch, getState }) => {
const { account, exchange, filters, registry } = getState()
const appliedFilters = payload === undefined ? filters : payload?.filters
console.log('payload', payload); // here payload always being undefined
console.log('appliedFilters', appliedFilters);
// remaining code goes here
}
In one of my file i am dispatching action as shwon below
useEffect(() => {
if(open) {
localStorageData?.forEach(item => {
const specKey = contractSpecKey(item)
if(!contracts.hasOwnProperty(specKey)) {
const appliedFilters = {
tokenPair: item.tokenPair,
optionStyle: item.optionStyle,
contractType: item.contractType,
expiryDate: item.expiryDate,
lotSize: item.lotSize
}
dispatch(jsonApiFetchOiAndLTPs({filters: appliedFilters}))
// here i am dispatching the action
}
})
}
}, [open])
For each items of localstorage data i am dispatching action based on if consition
I have a function "sendMessage" in React class:
class MessageForm extends React.Component {
...
sendMessage = async () => {
const { message } = this.state;
if (message) {
this.setState({ loading: true });
if (this.props.isPrivateChannel === false) {
socket.emit("createMessage", this.createMessage(), (response) => {
this.setState({ loading: false, message: "", errors: [] });
});
} else {
if (this.state.channel && this.state.channel._id === undefined) {
socket.emit("createChannelPM", this.state.channel, async (response) => {
const chInfo = { ...response, name: this.props.currentChannel.name };
console.log("chInfo : ", chInfo);
await this.props.setCurrentChannel(chInfo).then((data) => {
if (data) {
console.log("data : ", data);
console.log("this.props.currentChannel : ", this.props.currentChannel);
}
});
});
}
...
function mapStateToProps(state) {
return {
isPrivateChannel: state.channel.isPrivateChannel,
currentChannel: state.channel.currentChannel,
};
}
const mapDispatchToProps = (dispatch) => {
return {
setCurrentChannel: async (channel) => await dispatch(setCurrentChannel(channel)),
}
};
Here, in sendMessage function, I retrieve "response" from socket.io, then put this data into variable "chInfo" and assign this to Redux state, then print it right after assinging it.
And Redux Action function, "setCurrentChannel" looks like:
export const setCurrentChannel = channel => {
return {
type: SET_CURRENT_CHANNEL,
payload: {
currentChannel: channel
}
};
};
Reducer "SET_CURRENT_CHANNEL" looks like:
export default function (state = initialState, action) {
switch (action.type) {
case SET_CURRENT_CHANNEL:
return {
...state,
currentChannel: action.payload.currentChannel
};
...
The backend Socket.io part look like (I use MongoDB):
socket.on('createChannelPM', async (data, callback) => {
const channel = await PrivateChannel.create({
...data
});
callback(channel)
});
The console.log says:
Problem : The last output, "this.props.currentChannel" should be same as the first output "chInfo", but it is different and only print out previous value.
However, in Redux chrome extension, "this.props.currentChannel" is exactly same as "chInfo":
How can I get and use newly changed Redux states immediately after assinging it to Redux State?
You won't get the updated values immediately in this.props.currentChannel. After the redux store is updated mapStateToProps of MessageForm component is called again. Here the state state.channel.currentChannel will be mapped to currentChannel. In this component you get the updated props which will be accessed as this.props.currentChannel.
I believe you want to render UI with the latest data which you which you can do.
I'm using jest to test a vue application, I have a doubt how can I test a plugin code. This is the code I'm trying to test:
export const persistPlugin = store => {
store.subscribe(async (mutation, state) => {
// filter all keys that start with `__`
const _state = omitPrivate(state);
const storedState = await storage.get('state');
if (isEqual(_state, storedState)) return;
storage.set(store, 'state', _state);
});
};
What I'm stuck at is the store.subscribe part. store is passes as argument of the plugin method, but I don't know how to call this method from the test is a wat that triggers the function block of the plugin.
You could use testPlugin helper for this. Here it is an example which you could adapt for the state verification.
I prefer to track mutations instead of direct state changes:
import { persistPlugin } from "#/store";
export const testPlugin = (plugin, state, expectedMutations, done) => {
let count = 1;
// mock commit
const commit = (type, payload) => {
const mutation = expectedMutations[count];
try {
expect(type).toEqual(mutation.type);
if (payload) {
expect(payload).toEqual(mutation.payload);
}
} catch (error) {
done(error);
}
count++;
if (count >= expectedMutations.length) {
done();
}
};
// call the action with mocked store and arguments
plugin({
commit,
state,
subscribe: cb =>
cb(expectedMutations[count - 1], expectedMutations[count - 1].payload)
});
// check if no mutations should have been dispatched
if (expectedMutations.length === 1) {
expect(count).toEqual(1);
done();
}
};
describe("plugins", () => {
it("commits mutations for some cases", done => {
testPlugin(
persistPlugin,
{ resume: { firstName: "Old Name" } },
[{ type: "updateResume", payload: { firstName: "New Name" } }], // This is mutation which we pass to plugin, this is payload for plugin handler
[{ type: "updateResume", payload: { firstName: "New Name" } }], // This is mutation we expects plugin will commit
done
);
});
});
I have setup vuex and i would like to later fetch the data and update my form model but this fails
In my vuex
//state
const state = {
profile: [],
}
//getter
const getters = {
profileDetails: state => state.profile,
}
//the actions
const actions = {
getProfileDetails ({ commit }) {
axios.get('/my-profile-details')
.then((response) => {
let data = response.data;
commit(types.RECEIVED_USERS, {data});
},
);
}
}
const mutations = {
[types.RECEIVED_USERS] (state, { data }) {
state.profile = data;
state.dataloaded = true;
},
}
Now in my vue js file
export default{
data: () => ({
profile_form:{
nickname:'',
first_name:'',
last_name:'',
email:''
}
}),
computed:{
...mapGetters({
user: 'profileDetails',
}),
},
methods:{
setUpDetails(){
this.profile_form.email = this.user.email; //the value is always undefined
}
},
mounted(){
this.$store.dispatch('getProfileDetails').then(
(res)=>{
console.log(res); //this is undefined
this.setUpDetails(); ///this is never executed
}
);
this.setUpDetails(); //tried adding it here
}
By checking with the vue developer tools i can see that the vuex has data but my component cant fetch the data in vuex after calling the dispatch in the action to fetch the data.
Where am i going wrong.
Nb: AM using the data to update a form like this
<input v-model="profile_form.email" >
Your mounted method expects a return (res) from getProfileDetails, but the action isn't returning anything, so you could simply try
const actions = {
getProfileDetails ({ commit }) {
return axios.get('/my-profile-details')
.then((response) => {
let data = response.data;
commit(types.RECEIVED_USERS, {data});
return data // put value into promise
},
);
}
}
However, it's more usual to commit to store from within the action (which you are doing) and let the component get the new values from a getter (which you have) - i.e one-way-data-flow.
This is how I'd set it up.
data: () => ({
profile_form:{
nickname:'',
first_name:'',
last_name:'',
email:''
}
}),
mounted(){
this.$store.dispatch('getProfileDetails')
}
computed: {
...mapGetters({
user: 'profileDetails',
}),
}
watch: {
user (profileData){
this.profile_form = Object.assign({}, profileData);
}
},
methods:{
submit(){
this.$store.commit('submituser', this.profile_form)
}
},
I have been following these testing guidelines to test my vuex store.
But when I touched upon the actions part, I felt there is a lot going on that I couldn't understand.
The first part goes like:
// actions.js
import shop from '../api/shop'
export const getAllProducts = ({ commit }) => {
commit('REQUEST_PRODUCTS')
shop.getProducts(products => {
commit('RECEIVE_PRODUCTS', products)
})
}
// actions.spec.js
// use require syntax for inline loaders.
// with inject-loader, this returns a module factory
// that allows us to inject mocked dependencies.
import { expect } from 'chai'
const actionsInjector = require('inject!./actions')
// create the module with our mocks
const actions = actionsInjector({
'../api/shop': {
getProducts (cb) {
setTimeout(() => {
cb([ /* mocked response */ ])
}, 100)
}
}
})
I infer that this is to mock the service inside the action.
The part which follows is:
// helper for testing action with expected mutations
const testAction = (action, payload, state, expectedMutations, done) => {
let count = 0
// mock commit
const commit = (type, payload) => {
const mutation = expectedMutations[count]
expect(mutation.type).to.equal(type)
if (payload) {
expect(mutation.payload).to.deep.equal(payload)
}
count++
if (count >= expectedMutations.length) {
done()
}
}
// call the action with mocked store and arguments
action({ commit, state }, payload)
// check if no mutations should have been dispatched
if (expectedMutations.length === 0) {
expect(count).to.equal(0)
done()
}
}
describe('actions', () => {
it('getAllProducts', done => {
testAction(actions.getAllProducts, null, {}, [
{ type: 'REQUEST_PRODUCTS' },
{ type: 'RECEIVE_PRODUCTS', payload: { /* mocked response */ } }
], done)
})
})
This is where it I find it difficult to follow.
My store looks like:
import * as NameSpace from '../NameSpace'
import { ParseService } from '../../Services/parse'
const state = {
[NameSpace.AUTH_STATE]: {
auth: {},
error: null
}
}
const getters = {
[NameSpace.AUTH_GETTER]: state => {
return state[NameSpace.AUTH_STATE]
}
}
const mutations = {
[NameSpace.AUTH_MUTATION]: (state, payload) => {
state[NameSpace.AUTH_STATE] = payload
}
}
const actions = {
[NameSpace.ASYNC_AUTH_ACTION]: ({ commit }, payload) => {
ParseService.login(payload.username, payload.password)
.then((user) => {
commit(NameSpace.AUTH_MUTATION, {auth: user, error: null})
})
.catch((error) => {
commit(NameSpace.AUTH_MUTATION, {auth: [], error: error})
})
}
}
export default {
state,
getters,
mutations,
actions
}
And This is how I am trying to test:
import * as NameSpace from 'src/store/NameSpace'
import AuthStore from 'src/store/modules/authorization'
const actionsInjector = require('inject!../../../../../src/store/modules/authorization')
// This file is present at: test/unit/specs/store/modules/authorization.spec.js
// src and test are siblings
describe('AuthStore Actions', () => {
const injectedAction = actionsInjector({
'../../Services/parse': {
login (username, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve({})
} else {
reject({})
}
}, 300)
})
}
}
})
it('Gets the user profile if the username and password matches', () => {
const testAction = (action, payload, state, mutations, done) => {
const commit = (payload) => {
if (payload) {
expect(mutations.payload).to.deep.equal(payload)
}
}
action({ commit, state }, payload)
.then(result => {
expect(state).to.deep.equal({auth: result, error: null})
})
.catch(error => {
expect(state).to.deep.equal({auth: [], error: error})
})
}
testAction(injectedAction.login, null, {}, [])
})
})
If I try to do this, I get:
"Gets the user profile if the username and password matches"
undefined is not a constructor (evaluating 'action({ commit: commit, state: state }, payload)')
"testAction#webpack:///test/unit/specs/store/modules/authorization.spec.js:96:13 <- index.js:26198:14
webpack:///test/unit/specs/store/modules/authorization.spec.js:104:15 <- index.js:26204:16"
I need help understanding what am I supposed to do to test such actions.
I know it's been awhile but I came across this question because I was having a similar problem. If you were to console.log injectedActions right before you make the testAction call you'd see that the injectedAction object actually looks like:
Object{default: Object{FUNC_NAME: function FUNC_NAME(_ref) { ... }}}
So the main solution here would be changing the testAction call to:
testAction(injectedAction.default.login, null, {}, [], done)
because you are exporting your action as defaults in your store.
A few other issues that are unrelated to your particular error... You do not need to manipulate the testAction boilerplate code. It will work as expected so long as you pass in the proper parameters. Also, be sure to pass done to testAction or your test will timeout. Hope this helps somebody else who comes across this!