I use Object.assign() in several projects for a while and so far didn't have any unexpected experiences with the function. Today, I worked on some "auto saving" functionality and then realized that I need to "pause" (disable) the auto-saving feature for a moment to avoid double requests to the server. I went for a simple approach here to add a pause attribute to data.
So this is the solution I came up with (shortened):
let app = new Vue({
el: '#app',
data: {
pause: false,
form: {
title: '',
slug: '',
content: ''
}
},
watch: {
form: {
deep: true,
handler (newForm, oldForm) {
if (this.pause) {
return
}
this.debounceSave(newForm, oldForm)
}
}
},
methods: {
async save(newForm, oldForm) {
await this.createPost()
},
async createPost() {
try {
let response = await axios.post('https://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: this.form.content,
userId: 1
})
// disable auto saving....
this.pause = true
// I know the assignment doesnt make sense, just for testing reasons
Object.assign(this.form, {
title: response.data.title,
content: response.data.body
})
// and enable auto saving...
this.pause = false
} catch (error) {
//
}
},
},
created() {
this.debounceSave = _.debounce((n, o) => this.save(n,o), 300)
},
async mounted() {
//
}
})
I noticed tough that if I use Object.assign then the auto saving feature is not disabled and pause remains "false", hence not deactivating the auto saving feature (bad...).
I played around it and could solve the problem by using a promise:
async createPost() {
try {
let response = await axios.post('https://jsonplaceholder.typicode.com/posts', {
title: 'foo',
body: this.form.content,
userId: 1
})
// disable auto saving....
this.pause = true
// I know the assignment doesnt make sense, just for testing reasons
new Promise((resolve, reject) => {
Object.assign(this.form, {
title: response.data.title,
content: response.data.body
})
resolve()
})
.then(() => this.pause = false)
// and enable auto saving...
//this.pause = false
} catch (error) {
//
}
},
},
When using a promise I had to make sure to uncomment
// this pause = false
Which just increased my confusing as this shouldn't have any effect (I might be wrong...).
So the question now is: What is going on? Why does the initial approach not work? Why do I have to wrap it inside a Promise to make it ? Is it because of the "shallow copy" nature of Object assign? And if yes, can somebody maybe explain it?
Here's a snippet:
let app = new Vue({
el: '#app',
data: {
pause: false,
form: {
title: '',
slug: '',
content: ''
}
},
watch: {
form: {
deep: true,
handler(newForm, oldForm) {
if (this.pause) {
return
}
this.debounceSave(newForm, oldForm)
}
}
},
methods: {
async save(newForm, oldForm) {
await this.createPost()
},
async createPost() {
try {
let response = await axios.post('https://jsonplaceholder.typicode.com/posts', {
title: this.form.title,
body: this.form.content,
userId: 1
})
this.pause = true
new Promise((resolve, reject) => {
Object.assign(this.form, {
title: response.data.title,
content: response.data.body
})
resolve()
})
.then(() => this.pause = false)
//this.pause = false
} catch (error) {
//
}
},
},
created() {
this.debounceSave = _.debounce((n, o) => this.save(n, o), 300)
},
async mounted() {
//
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js" integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ==" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.12/dist/vue.js"></script>
<div id="app">
<h1>{{ pause }}</h1>
Title <input type="text" v-model="form.title"><br> Content <input type="text" v-model="form.content">
<p>{{ form }}</p>
</div>
This behavior has nothing to do with Object.assign. You need to understand how Vue’s watcher does its job under the hood.
Long story short, the handler callback, by default, is not synchronously called, it’s scheduled asynchronously till before next render or next tick.
When you synchronously set pause=true, trigger the watcher, then set pause=false, by the time the async handler is called, pause is already set back to false. This also explains why the promise version works, cus promise is async too.
If you want handler to be a sync call, you need to also set the flush: "sync" watch option in v3, in v2 I guess it’s sync: true (pls double check yourself).
Related
I am using Nuxt in SPA mode and have a page structure like this:
pages
...
- users/
- - index
- - new
- - update/
- - - _id
...
I have a page of users with a list of them and 'subpage' - new.
On my users/index page I am fetching my users in asyncData Hook like this:
async asyncData({ app: { apolloProvider }, store: { commit, dispatch } }) {
const {
data: { getAllUsers: { success, users, message } },
} = await apolloProvider.defaultClient.query({
query: getAllUsersGQL,
})
if(success) {
await commit('user/setUsers', users, { root: true })
} else {
dispatch('snackbar/notify', { message: message, type: 'error' }, { root: true })
}
},
It seems to work as it should. But when I go to my page users/new, fill up the form and send it, update the store and redirect to my users/index page, I encounter kinda interesting behaviour.
The problem here is that I don't have a newly updated state but some kinda cached one or previous state. I can so far make it working with location.replace. When the page reloads I have an accurate and updated state.
That's how I'm handling redirect on users/new page:
async save() {
if(this.$refs.create.validate()) {
this.loading = true
delete this.form.confirm
await this.createUserStore(this.form)
this.$router.push(
this.localeLocation({
name: 'users',
})
)
this.loading = false
this.$refs.create.reset()
}
},
and that's how I am refreshing my state in Vuex:
export const mutations = {
updateUsers: (state, payload) => {
state.users = [...state.users, payload].sort((a,b) => a.createdAt - b.createdAt)
},
}
That's how I'm passing data:
computed: {
...mapGetters({
storeUsers: 'user/getUsers',
storeGetMe: 'auth/getMe',
}),
},
<v-data-table
:headers="headers"
:items="storeUsers"
:search="search"
item-key="id"
class="elevation-1"
dense
>
</v-data-table>
I already tried to list items using v-for and it doesn't work either.
And when I console.log state I get all items. It works as it should.
What can be the problem that it's not updating the view?
If anyone has ever faced such kind of behaviour I'd appreciate any hints.
This is probably coming from the fact that Apollo does have it's own cache and that it reaches for the cache first as cache-first is the default value.
Give this one a try
await apolloProvider.defaultClient.query({
query: getAllUsersGQL,
fetchPolicy: 'network-only',
})
Additional answer
This is an example of a dynamic GQL query that I previously wrote
test.gql.js
import { gql } from 'graphql-tag'
import { constantCase, pascalCase } from 'change-case'
export const queryCompanyBenefitInfo = ({
benefitType,
needCifEligibility = false,
needActiveOnWeekend = false,
needCompanyContribution = false,
needAutoRenewed = false,
needUnitValue = false,
}) => {
return gql`
query {
CompanyBenefit {
oneOfType(benefitType: ${constantCase(benefitType)}) {
... on Company${pascalCase(benefitType)}Benefit {
${needCifEligibility ? 'cifEligibility' : ''}
${needActiveOnWeekend ? 'activeOnWeekend' : ''}
${needCompanyContribution ? 'companyContribution' : ''}
${needAutoRenewed ? 'autoRenewed' : ''}
${
needUnitValue
? `unitValue {
value
}`
: ''
}
}
}
}
}
`
}
And call it this way
import { testQuery } from '~/apollo/queries/test.gql.js'
...
await this.app.apolloProvider.defaultClient.query({
query: testQuery({ benefitType: 'care', needCifEligibility: true }),
fetchPolicy: 'network-only',
errorPolicy: 'all',
})
I have a hook..
export function useLazyProposalList() {
const [getQueueData, { loading, data, error, fetchMore }] = useLazyQuery(PROPOSAL_LIST, {
fetchPolicy: 'no-cache',
});
const proposalList = React.useMemo(() => {
if (!data) {
return null;
}
return transformProposals(data);
}, [data]);
return {
getQueueData,
fetchMore,
loading,
data: proposalList,
error,
};
}
In the component
const {
getQueueData,
data: queueData,
fetchMore: fetchMoreProposals,
// loadMore: loadMore,
} = useLazyProposalList();
If user clicks on fetch more button, I call: fetchMoreProposals .
await fetchMoreProposals({
variables: {
offset: visibleProposalList.length,
},
});
but this doesn't update my data. I read that we should use offsetLimitPagination, but my data from query is not array itself. It's like this: queue { id: '1', items:[] } and because of this, offsetLimitPagination doesn't work. So I tried merge
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
queue: {
keyArgs: false,
merge(existing, incoming) {
console.log(existing, incoming);
if (!incoming) return existing;
if (!existing) return incoming;
},
},
},
},
}
but in the console, it just prints refs instead of real data.
What could be the issue ?
I have a saga that yields a put. One of the arguments to that put is a function object.
I've copied the saga function only as far as it is relevant:
function* updateTaskTimeCancelledTimeRejectedSuccess(action) {
const {payload} = action.data
const tasks = yield select(getTasksSelector);
const parent = yield call(findExistingTaskParent, tasks, action.data.taskUUID);
if (action.type === taskActions.updateTaskCancelledTimeActions.success) {
const currentValue = parent.taskGroup[action.data.taskUUID].time_cancelled;
if (currentValue === null) {
// only notify if marking rejected for the first time
const restoreActions = yield () => [taskActions.updateTaskCancelledTimeRequest(
action.data.taskUUID,
{time_cancelled: null}
)];
const viewLink = `/task/${encodeUUID(action.data.taskUUID)}`
yield put(displayInfoNotification("Task marked cancelled", restoreActions, viewLink))
This is the test:
it("updates the task state with cancelled time and sends a notification", () => {
const action = {type: taskActions.updateTaskCancelledTimeActions.success, data: {taskUUID: "someUUID", time_cancelled: new Date().toISOString()}};
const restoreActions = () => [taskActions.updateTaskCancelledTimeRequest(
action.data.taskUUID,
{time_cancelled: null}
)];
const viewLink = `/task/${encodeUUID(action.data.taskUUID)}`
return expectSaga(testable.updateTaskTimeCancelledTimeRejectedSuccess, action)
.provide([
[select(getTasksSelector), {
tasksNew: {
1: {
someUUID: {
time_cancelled: null,
parent_id: 1
}
}
}
}]])
.put(displayInfoNotification("Task marked cancelled", restoreActions, viewLink))
.run()
})
})
and this is the result:
SagaTestError:
put expectation unmet:
Expected
--------
{ '##redux-saga/IO': true,
combinator: false,
type: 'PUT',
payload:
{ channel: undefined,
action:
{ type: 'DISPLAY_INFO_NOTIFICATION',
message: 'Task marked cancelled',
restoreActions: [Function: restoreActions],
viewLink: '/task/0000000000000000000000' } } }
Actual:
------
1. { '##redux-saga/IO': true,
combinator: false,
type: 'PUT',
payload:
{ channel: undefined,
action:
{ type: 'DISPLAY_INFO_NOTIFICATION',
message: 'Task marked cancelled',
restoreActions: [Function],
viewLink: '/task/0000000000000000000000' } } }
at node_modules/redux-saga-test-plan/lib/expectSaga/expectations.js:48:13
at node_modules/redux-saga-test-plan/lib/expectSaga/index.js:544:7
at Array.forEach (<anonymous>)
at checkExpectations (node_modules/redux-saga-test-plan/lib/expectSaga/index.js:543:18)
It's obvious to me that the equality is failing because of the restoreActions object which is a function of a different reference. I can't figure out what I should do instead to make the test pass. What do I need to do to verify the saga is flowing as I expected? I don't necessarily need to verify the result of the function, just that the displayInfoNotification put took place.
Thank you.
Make a factory function for your restoreActions lambda and then you can mock it for your test.
e.g.
// somewhere in your module
const actionRestoreFactory = (action) => () => [taskActions.updateTaskCancelledTimeRequest(action.data.taskUUID, {time_cancelled: null})]
//In your saga
yield put(displayInfoNotification("Task marked cancelled", actionRestoreFactory(action), viewLink))
//In your test
import actionRestoreFactory from './your/module'
jest.mock('./your/module')
actionRestoreFactory.mockReturnValue(jest.fn())
//...
return expectSaga(testable.updateTaskTimeCancelledTimeRejectedSuccess, action)
.provide([ ... ])
.put(displayInfoNotification("Task marked cancelled", actionRestoreFactory(action), viewLink))
.run()
App.vue
template:
<ResponsiveNavigation
:nav-links="navLinks"
/>
script
data: () => ({
navLinks: []
}),
created: function() {
this.getSocialNetworks();
},
methods: {
getSocialNetworks() {
var self = this;
axios
.get(MY_API_URL)
.then(function(res) {
var fb_url = res.data.data.filter(obj => {
return obj.key === "Social_Facebook";
});
self.navLinks.fb = fb_url[0].defaultValue;
//
var ig_url = res.data.data.filter(obj => {
return obj.key === "Social_Instagram";
});
self.navLinks.ig = ig_url[0].defaultValue;
//
})
.catch(function(error) {
console.log("Error", error);
});
}
}
ResponsiveNavigation.vue:
<a :href="$props.navLinks.fb"></a>
if I console.log the $props.navLinks I have everything stored.
however in the href doesn't work after the FIRST load.
I am fairly sure that this is due to the reactive nature and UNreactive of arrays.
You're not really using an array, but an object
data: () => ({
navLinks: []
}),
to
data: () => ({
navLinks: {
fb:'',
ig:''}
}),
and I think it would setup the reactive props more suitably.
If you need an array, then use array.push() so it can react accordingly. I may also consider moving it to the mounted() method. Finally, you put $props in your code, do you have other props you've not shown us which may be conflicting?
When I load my page my datacollection is null or undefined, but I want my data from the api to be filled in as soon as the site loads.
<script>
import LineChart from './LineChart.js'
import axios from 'axios'
export default {
name: 'Graph',
components: {
LineChart
},
data () {
return {
ratingOne: null,
ratingTwo: null,
ratingThree: null,
datacollection: null
}
},
created: function () {
console.log(this.datacollection)
this.fillData()
},
mounted () {
},
methods: {
getZeroes () {
axios.get('MYAPI').then(response => {
this.ratingOne = response.data.Rates.filter(rates =>
rates.rate === 0).length
return this.ratingOne
})
},
fillData () {
this.getOnes()
console.log(this.getZeroes())
this.getOnes()
this.getTwos()
this.datacollection = {
labels: ['Dårlig oplevelse', 'Okay oplevelse', 'Rigtig god
oplevelse'],
datasets: [
{
backgroundColor: ['#FF0000', '#D3D3D3', '#00CC00'],
data: [this.ratingOne, this.ratingTwo, this.ratingThree]
}
]
}
}
}
}
</script>
When i use a click function it works and it loads my graph with the data I want, but it does not work when my page I loaded. Can anyone tell me why?
When i console log "this.getZeroes())" it just tells me "undefined"
and when i click my button with update
<button #click="fillData()">Randomize</button>
it works
getZeroes, getOnes, getTwos are asynchronous functions. After getZeroes function and such are run, the data is not ready. That's why dataCollection doesn't have the right data.
You need to you promise to wait for the response finish, and update the data to the chart.
getZeroes will return a promise.
getZeroes () {
return axios.get('MYAPI').then(response => {
this.ratingOne = response.data.Rates.filter(rates => rates.rate === 0).length
return this.ratingOne
})
},
in the fillData. We must wait them to finish by using then
fillData () {
this.getOnes()
.then(() => this.getZeroes())
.then(() => this.getTwos())
.then(() => {
this.datacollection = {
labels: ['Dårlig oplevelse', 'Okay oplevelse', 'Rigtig god oplevelse'],
datasets: [
{
backgroundColor: ['#FF0000', '#D3D3D3', '#00CC00'],
data: [this.ratingOne, this.ratingTwo, this.ratingThree]
}
]
}
})
}
Or we can even run the gets parallel and get data at once by using Promise.all
Promise.all([this.getOnes(), this.getTwos(), this,getThrees()])
.then(() => { /* the rest here */ })