What is the proper way to call a getter in vuex after you dispatched an async action which mutated the state?
I created an example snippet to illustrate what I mean. As you can see, getLastNameByName() fails because state.persons is empty. The weird thing is, that if I print state.persons in that getter, it prints the array after api call.
Expected behaviour is that getLastNameByName('John') returns {name: 'John', lastname: 'Smith'}
const store = new Vuex.Store({
state: {
persons: []
},
getters: {
getLastNameByName: (state) => (name) => {
// console.log(state.persons) returns the state, yet I cannot call .find on it
return state.persons.find(element => {
return element.name === name
}).lastname
},
},
mutations: {
setPersons: (state, payload) => {
state.persons = [...payload]
}
},
actions: {
async getPeople({commit}) {
return new Promise(function(resolve, reject) {
setTimeout(async () => {
commit('setPersons', [{
name: 'John',
lastname: 'Smith'
}, {
name: 'Sarah',
account: 'Appleseed'
}])
resolve();
}, 1000)
})
}
}
})
new Vue({
store,
el: '#app',
mounted() {
this.$store.dispatch('getPeople').then( () => {
console.log(this.$store.getters.getLastNameByName('John'))
})
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app">
</div>
setTimeout() don't returns awaitable object. Check with promise:
const store = new Vuex.Store({
state: {
persons: []
},
getters: {
getLastNameByName: (state) => (name) => {
return state.persons.find(element => {
return element.name === name
}).lastname
},
},
mutations: {
setPersons: (state, payload) => {
state.persons = [...payload]
}
},
actions: {
async getPeople({commit}) {
return new Promise(function(resolve, reject) {
setTimeout(async () => {
commit('setPersons', [{
name: 'John',
lastname: 'Smith'
}, {
name: 'Sarah',
account: 'Appleseed'
}])
resolve();
}, 1000)
})
}
}
})
new Vue({
store,
el: '#app',
mounted() {
this.$store.dispatch('getPeople').then(() => {
console.log(this.$store.getters.getLastNameByName('John'));
})
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/vuex"></script>
<div id="app">
</div>
Anyway direct handling async calls to store isn't a proper way. I think in this case better solutions is to watch store state or use computed properties.
Tried on jsbin.com with few improvements and no problem:
<!DOCTYPE html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>Vue example</title>
<div id="app">
<show-person
:name="getLastName('John')"
></show-person>
</div>
<script src="https://unpkg.com/vue#2.5.21/dist/vue.min.js"></script>
<script src="https://unpkg.com/vuex#3.0.1/dist/vuex.min.js"></script>
<script>
const store = new Vuex.Store({
state: {
persons: []
},
getters: {
getLastName: state => name => {
return state.persons.length
? state.persons.find(element => {
return element.name === name
}).lastname
: ''
}
},
mutations: {
setPersons: (state, payload) => {
state.persons = [...payload]
}
},
actions: {
getPeople: ({ commit }) => new Promise(res => {
const data = [
{name: 'John', lastname: 'Smith'},
{name: 'Sarah', account: 'Appleseed'}
]
setTimeout(() => {
commit('setPersons', data)
res()
}, 1000)
})
}
})
const ShowPerson = {
functional: true,
render: (h, ctx) => h('p', ctx.props.name)
}
new Vue({
store,
el: '#app',
components: {
ShowPerson
},
computed: {
...Vuex.mapGetters([
'getLastName'
])
},
methods: {
...Vuex.mapActions([
'getPeople'
])
},
created () {
this.getPeople()
}
})
</script>
Related
When I was building vue3ssr recently, everything went smoothly, but the store data on the server side could never cover the store data on the client side. The following is my code, please help me to see where there is a problem
// entry-client.js
if (window && window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}
router.isReady().then(() => {
app.mount('#app')
})
<script>
<template>
<div class="home">
<h1>This is Home</h1>
<h2>ssr msg: {{ msg }}</h2>
</div>
</template>
<script setup>
import useStore from "#/store";
import { computed } from "vue";
const store = useStore();
const msg = computed(() => { // => msg: ''
return store.state.msg;
});
// console.log("store", store);
</script>
<script>
// views/home.vue
export default {
asyncData: (store) => {
return store.dispatch("asyncSetMsg");
},
};
</script>
</script>
// store/index.ts
export default function useStore() {
return createStore({
state: {
msg: "",
},
getters: {},
mutations: {
SET_MSG(state, payload) {
state.msg = payload;
},
},
actions: {
// store/index.ts
asyncSetMsg({ commit }) {
return new Promise<void>((resolve, reject) => {
setTimeout(() => {
commit("SET_MSG", "This is some msg in ssr");
resolve();
}, 300);
});
},
},
modules: {},
});
}
// entry-server.js
Promise.all(componentArr).then(() => {
console.log('store.state', store.state); // => {msg: 'This is some msg in ssr'}
html += `<script>window.__INITIAL_STATE__ = ${replaceHtmlTag(JSON.stringify(store.state))}</script>`
resolve(html);
}).catch(() => {
reject(html)
})
The above is the relevant code The following is the running result, I tried to print on the server side and the client side, the result is the normal output on the server side, but there is no data on the client sid
I m using vis.js timeline and i want display date from firestore. It works when I type manually (look --> this.items), but does not work with firestore (look --> this.users).
I m using Vue framework.
<script>
export default {
data() {
return {
users: [],
items: [
{
id: '1',
content: 'London',
group: 'Mike',
start: '2021-12-20',
end: '2022-06-19',
},
],
}
},
async fetch() {
await this.loadPlaces()
},
methods: {
async loadPlaces() {
const querySnapshot = await getDocs(collection(db, 'places'))
querySnapshot.forEach((doc) => {
this.users.push({ id: doc.id, ...doc.data() })
})
this.$store.commit('places/setPlaces', this.users)
},
},
computed: {
places() {
return this.$store.state.places.places
},
},
mounted() {
let container = document.getElementById('visualization')
let options = {
moveable: true,
}
let timeline = new vis.Timeline(container)
timeline.setOptions(options)
timeline.setGroups(this.groups)
timeline.setItems(this.items)
},
}
</script>
I found a solution.
I just moved all code from mounted() to method loadPlaces (under this.$store.commit)
Save yourself trouble and use the vis datasets instead.
my pinia store in vue 3 looks like this.
import { defineStore } from 'pinia'
import { DataSet } from 'vis-data/esnext'
export const useVisData = defineStore('visData', {
state: () => ({
items: new DataSet([]),
groups: new DataSet([]),
selectedItems: [],
serializedGroupsAndItems: []
}),
actions: {
//Group actions
showAllGroups() {
this.groups.forEach(group => {
this.groups.updateOnly({ id: group.id, visible: true })
});
},
addGroup(group) {
this.groups.add(group)
},
hideGroup(group) {
this.groups.updateOnly({ id: group, visible: false })
},
//Item actions
addItem(item) {
this.items.add(item)
},
removeItem(item) {
this.items.remove(item)
},
setSelectedItems(items) {
this.selectedItems = items
},
//data add/remove
serializeData() {
this.serializedGroupsAndItems.push({
groups: JSON.stringify(this.groups.get()),
items: JSON.stringify(this.items.get())
})
},
loadSerializedData() {
this.clearGroupsAndItems()
this.serializedGroupsAndItems.forEach(data => {
this.addGroup(JSON.parse([data.groups]))
this.addItem(JSON.parse([data.items]))
})
},
//misc
clearGroupsAndItems() {
this.groups.clear()
this.items.clear()
}
},
getters: {
getHiddenGroups(state) {
return state.groups.get({
filter: (item) => {
return item.visible === false
}
})
}
}
})
Also remember to watch for changes in your options.
Might be better to wrap it in a vue component too. something like this.
this is what i did.
let timeline;
const visref = ref(null);
onMounted(async () => {
timeline = new Timeline(visref.value, props.items, props.groups, {...props.options, ...timelineOptions});
props.events.forEach(event => {
on(event, (properties) => {
// console.log(event, properties)
emits(`vis${event}`, properties);
});
});
})
<template>
<div ref="visref"></div>
</template>
then you can use it like so:
const timelineref = ref();
<Timeline
ref="timelineref"
:items="visStore.items"
:groups="visStore.groups"
:options="options"
/>
remember to expose the instance in your timeline component then you can call the functions using a ref like this.
timelineref.value.timeline.zoomOut(0.5)
In the docs for testing incrementing todo ids, this assumes a predictable response.
In an example such as below, a unique id is generated.
How could this be tested?
This test passes, but I'm not sure if it's correct, shouldn't the id be defined based on what's in the prepare callback?
slice.js
add: {
reducer: (state, {payload}: PayloadAction<{id: string, item: Item}>) => {
state[payload.id] = payload.item
},
prepare: (item: Item) => ({
payload: {id: cuid(), item}
})
}
slice.test.js
it('should handle add', () => {
expect(
reducer(
{},
{
type: actions.add,
payload: {
id: 'id-here?',
item: {
other: 'properties...'
}
},
}
)
).toEqual({
'id-here?': {
other: 'properties...'
},
})
})
You can pull out the prepare function and also the reducer function into it's own constant and then test prepare in isolation:
todosSlice.js:
[...]
let nextTodoId = 0;
export const addTodoPrepare = (text) => {
return {
payload: {
text,
id: nextTodoId++
}
}
}
export const addTodoReducer = (state,
action) => {
const {id, text} = action.payload;
state.push({
id,
text,
completed: false
});
};
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: {
prepare: addTodoPrepare,
reducer: addTodoReducer,
},
}
})
[...]
todosSlice.spec.js:
import todos, {addTodo, addTodoPrepare} from './todosSlice'
describe('addTodoPrepare',
() => {
it('should generate incrementing IDs',
() => {
const action1 = addTodoPrepare('a');
const action2 = addTodoPrepare('b');
expect(action1.payload).toEqual({
id: 0,
text: 'a'
})
expect(action2.payload).toEqual({
id: 1,
text: 'b'
})
})
})
describe('todos reducer',
() => {
[...]
})
For unit testing, NO, just test each reducer independently.
For integration testing and e2e testing, Yes.
I have the following controller action
[HttpPost]
[Route("api/Tenant/SetTenantActive")]
public async Task<IHttpActionResult> SetTenantActive(string tenantid)
{
var tenantStore = CosmosStoreFactory.CreateForEntity<Tenant>();
var allTenants = await tenantStore.Query().Where(x => x.TenantDomainUrl != null).ToListAsync();
foreach(Tenant ten in allTenants)
{
ten.Active = false;
await tenantStore.UpdateAsync(ten);
}
var tenant = await tenantStore.Query().FirstOrDefaultAsync(x => x.Id == tenantid);
if (tenant == null)
{
return NotFound();
}
tenant.Active = true;
var result = await tenantStore.UpdateAsync(tenant);
return Ok(result);
}
And my react code:
import React, { Component } from 'react';
import { Table, Radio} from 'antd';
import { adalApiFetch } from '../../adalConfig';
import Notification from '../../components/notification';
class ListTenants extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
fetchData = () => {
adalApiFetch(fetch, "/Tenant", {})
.then(response => response.json())
.then(responseJson => {
if (!this.isCancelled) {
const results= responseJson.map(row => ({
key: row.ClientId,
ClientId: row.ClientId,
ClientSecret: row.ClientSecret,
Id: row.Id,
SiteCollectionTestUrl: row.SiteCollectionTestUrl,
TenantDomainUrl: row.TenantDomainUrl
}))
this.setState({ data: results });
}
})
.catch(error => {
console.error(error);
});
};
componentDidMount(){
this.fetchData();
}
render() {
const columns = [
{
title: 'Client Id',
dataIndex: 'ClientId',
key: 'ClientId'
},
{
title: 'Site Collection TestUrl',
dataIndex: 'SiteCollectionTestUrl',
key: 'SiteCollectionTestUrl',
},
{
title: 'Tenant DomainUrl',
dataIndex: 'TenantDomainUrl',
key: 'TenantDomainUrl',
}
];
// rowSelection object indicates the need for row selection
const rowSelection = {
onChange: (selectedRowKeys, selectedRows) => {
if(selectedRows[0].key != undefined){
console.log(selectedRows[0].key);
const options = {
method: 'post',
body: {tenantid:selectedRows[0].key},
};
adalApiFetch(fetch, "/Tenant/SetTenantActive", options)
.then(response =>{
if(response.status === 200){
Notification(
'success',
'Tenant created',
''
);
}else{
throw "error";
}
})
.catch(error => {
Notification(
'error',
'Tenant not created',
error
);
console.error(error);
});
}
},
getCheckboxProps: record => ({
type: Radio
}),
};
return (
<Table rowSelection={rowSelection} columns={columns} dataSource={this.state.data} />
);
}
}
export default ListTenants;
focus only on the onchange event,
And the screenshot:
And it looks like the request gets to the webapi (I attached the debugger)
Update:
Basically If I dont put FromBody I need to send the parameter via querystring.
However if I put from Body and I send the parameter in the body, its received null on the webapi
Add [FromBody] before your input parameter in your action method like this:
public async Task<IHttpActionResult> SetTenantActive([FromBody] string tenantid)
Then, convert your selected row key into string
const options = {
method: 'post',
body: { tenantid : selectedRows[0].key.toString() }
};
Action "type": undefined, is what I keep getting returned when I try to test a redux action with fetch-mock. Any suggestion on how to resolve this issue? Could it be a bug in fetch-mock?
Expected value to equal:
[{"type": undefined}, {"result": {"hello": "world"}, "type": undefined}]
Received:
[{"type": "DASHBOARD_RESULT_LOADING"}, {"result": {"hello": "world"}, "type": "DASHBOARD_RESULT_READY"}]
dashboardActions.js
function resultReady(json) {
return {
type: DASHBOARD_RESULT_READY,
result: camelizeKeys(json)
};
}
export function requestPredict(params) {
let url = `${process.env.API_URL}/predict/`;
const requestParams = {
method: 'post',
credentials: 'include'
};
return async (dispatch) => {
return fetch(url, requestParams)
.then(response => {
if (response.status === 200) {
return response.json();
} else {
throw Error(response.statusText);
}
})
.then(data => dispatch(resultReady(data)));
};
}
dashboardActions.test.js
const mockData = {
"hello": "world"
}
describe('action creators', () => {
afterEach(() => {
fetchMock.reset()
})
it('should create DASHBOARD_RESULT_LOADING', () => {
fetchMock.post('*', {"hello":"world"} );
const expectedActions = [
{ type: actions.DASHBOARD_RESULT_LOADING },
{ type: actions.DASHBOARD_RESULT_READY, result: mockData }
]
const store = mockStore({ result: {}})
return store.dispatch(actions.requestPredict())
.then((data) => {
expect(store.getActions()).toEqual(expectedActions)
})
})
})
You're receiving types "DASHBOARD_RESULT_LOADING" and "DASHBOARD_RESULT_READY", which seems to be the intended behaviour. You're expecting actions.DASHBOARD_RESULT_LOADING and actions.DASHBOARD_RESULT_READY, neither of which seems to be defined with any value.
Either define actions.DASHBOARD_RESULT_LOADING and actions.DASHBOARD_RESULT_READY:
actions.DASHBOARD_RESULT_LOADING = 'DASHBOARD_RESULT_LOADING'
actions.DASHBOARD_RESULT_READY = 'DASHBOARD_RESULT_READY'
or replace them with your expected types:
const expectedActions = [
{
type: 'DASHBOARD_RESULT_LOADING'
},
{
type: 'DASHBOARD_RESULT_READY',
result: mockData
}
]