Change vue data from promise callback - javascript

I'm trying to change my Vue data from inside a promise callback,
and for some reason, I can't see the value changes in the template,
I saw this question how to update a vue's property in a promise call back?
and I've changed the this to the correct one, but still, I can't see the value changing in the template.
this is my template:
<template>
{{ sites }}
</template>
and this is my script:
data: () => ({
sites: {}
}),
methods: {
initialize(snapshot) {
let _this = this
snapshot.forEach((site) => {
convert(site.data())
.then(converted => {
_this.sites[site.id] = converted
})
})
}
}
any idea why my data won't change?

I think it's reactive issue. Please try
.then(converted => {
const sites = JSON.parse(JSON.stringify(_this.sites))
sites[site.id] = converted
_this.sites = sites
}
More explanation:
To update page, Vue does cheap comparison (as I understand, reference comparison). If you only update an attribute of object, they still point to same reference -> Vue will not update page. So we need to make deep copy them to make the comparison work

Related

Data from store not reactive in Vue.js 3

This is my first attempt at building a web app with Vuejs. I've been trying to get data from an external JSON API and display it on my app. The JSON fetch etc is working fine. but I can't get the data to be displayed reactively on my component.
As you can read in Appraisal.js given an API link some data is populated in Appraisal.app_data. The data always has an array called items (that's just how the API is. I'll add validation later). As a proof of concept I'm trying to display the number of elements in the items array.
Since other components in my app will also use this data, I'm using an external store as the data source everywhere. One of the components calls Appraisal.setLink() on getting some user input. That part is working as expected. However the DOM contents don't change at all.
I referred to State Management for setting up the external store. I also referred to some other answers on StackOverflow with a similar issue and got the following suggestions:
The data should be initialized to undefined or null instead of {} for reactivity to work.
Properties of objects are not reactive. But by my understanding this was changed in Vue3 where it doesn't matter because proxies are in use. Either way I tried using the Object.assign({}, ..., ...) method but it did not help.
Arrow functions cannot be used in methods for reactive objects. If I remove the arrow function and put the body inside .then(function(data) {...}) it complains that this is not defined for the second then function on fetch
// --- src/components/AppraisalView.vue
<script setup>
import ItemView from './ItemView.vue';
</script>
<template>
<div v-if="app_data">{{app_data.items.length}} items in appraisal</div>
<div v-else>Enter link to get quote</div>
</template>
<script>
import {Appraisal} from '../stores/Appraisal.js';
export default {
data() {
return {
app_data: Appraisal.app_data,
}
},
}
</script>
// ---- src/store/Appraisal.js
import {reactive} from 'vue'
import {BuybackCalculator} from './BuybackCalculator.js';
export const Appraisal = reactive({
link: '',
app_data: undefined,
methods: {
setLink(value) {
if (this.link == value) return;
this.link = value;
console.log('Updating appraisal with: '+this.link);
fetch(this.link+".json")
.then((response) => response.json())
.then((data) => {
this.app_data = data;
console.log(this.app_data);
BuybackCalculator.methods.calculate_buyback(this.app_data);
});
}
}
});

Having promise response inside Vue template interpolation

I'm using Vue 2 and the composition API. My current component receives a prop from a parent and I render different data based on it, on the screen - the prop is called "result" and is of type Object, containing multiple info. I receive one "result" at a time, but multiple will be rendered - you can think of my "result" as a google search result - which means that the page will have multiple results.
The issue I have is that for one of the info inside "result", I need to call an asynchronous method and display its result, which is what I cannot accomplish.
Currently this is what I have:
<div>
{{ getBoardName(props.result.boardReference) }}
</div>
Method inside the script:
async function getBoardName(boardReference) {
var result = await entriesApi.getBoardName({
boardReference: boardReference,
});
return result;
}
It displays "[object Promise]", although if I console.log(result) right before returning it, it's just what I need, so it seems to me as if the interpolation doesn't actually wait for the promise result.
I've also tried using "then" inside the interpolation:
{{
getBoardName(props.result.boardReference).then((value) => {
return value;
})
}}
I was thinking about using a computed property but I am not sure how that would work, as the response I need from the method has to be connected to each result individually.
Please ask for further clarifications if needed.
As you thought, the interpolation does not actually wait for the result so this is why you have a Promise object.
Since you are using the composition API, what you can actually do is to use a reactive state (using the ref() function if you are waiting for a primitive type, which seems to be the case here, or the reactive() function for objects) and update the state within the onMounted() lifecycle hook.
setup(props, context) {
const boardGameName = ref("");
onMounted(async () => {
boardGameName.value = await getBoardName(props.result.boardReference);
});
async function getBoardName(boardReference) {
var result = await entriesApi.getBoardName({
boardReference: boardReference,
});
return result;
}
return {
boardGameName,
};
}
Since you are dealing with async data, you could add a loading state so you can show a spinner or something else until the data is available.
If your board reference changes over time, you could use a watcher to update your board game name.
Good luck with your project!

Why using toRef(props) and is it good idea to change their value in CompositionAPI?

I've seen a pattern of using props in of CompositionAPI very often,
that is use toRefs to make all entries of props ref.
I'm kind of confused by it.
For exmaple, from the Vue 3 official guide:
export default {
props: {
user: {
type: String,
required: true
}
},
setup(props) {
const { user } = toRefs(props)
//... use user's value
}
}
I have 2 questions in 2 scenearios:
when the props.user is already reactive
Its value will change if it's changed in any ancestors, so why we need to use toRefs? Doesn't it already reactive?
if it's not reactive, it's just a primitive string value
Does making it reactive means we're going to change its value?
I think making a object reactive also imply that its value is welcomed to be changed.
But all the guides and linters warn us that we'd better not to change the props value.(for not writing to the computed value or something)
If I can change the props value directly in the component, I no longer need to emit the changes to parent component everytime.
It's very convenient but I don't know whenther it is a good idea to change the props value after we're sure it becomes reactive?
Since props aren't supposed to be mutated, this is useful for a specific case that is explained in the documentation; a ref that is computed from a prop needs to be passed as an argument:
const { user } = toRefs(props)
// or
// const user = computed(() => props.user)
someFunction(user);
Where a function makes use of composition API or just needs an argument to be passed by reference rather than by value due to the way it works, e.g.:
function someFunction(val) {
setTimeout(() => {
console.log('Up-to-date value:', unref(val));
}, 1000);
}

Vue.js - Add or set new data to array in store

I have two Vue components that use a common array set in a store like this:
var store = {
state: {
myArray: []
}
};
var FirstComp = Vue.extend({
template: '#first-template',
data: function () {
return {
arrayData: store.state.myArray
};
}
});
/* A second component quite identical */
I followed the example given in the Vue js guide.
I'm trying to update the data in the array in the store with new data from another array (after an ajax call), so that it impacts both components. I would like to have a nice way of replacing / concating the old array with a new one. I know I can't just replace the array like this store.state.myArray = newArrayData;because I would loose the Vue binding. But the method given in the docs (at least for concat) doesn't work in the case of the store (or maybe I'm missing something?).
Right now, the only way I've found is to use a foreach with push, $removeor $set depending on the operation and it is not that elegant and practical.
For example, for concat, I do this:
$.each(newArray, function (i, val) {
store.state.myArray.push(val);
});
But for replacing it gets uglier. What would be the proper way to this?
(For info, I'm not using Vuex for state management and I don't plan to at the moment, I'm keeping it very simple)
To make the assignment work you can just use "state" in your component like this:
var FirstComp = Vue.extend({
template: '#first-template',
data: function () {
return {
state: store.state
};
}
});
And then use state.myArray. This way if you will do store.state.myArray = newValue it won't break the binding.

React component not re-rendering on state change

I have a React Class that's going to an API to get content. I've confirmed the data is coming back, but it's not re-rendering:
var DealsList = React.createClass({
getInitialState: function() {
return { deals: [] };
},
componentDidMount: function() {
this.loadDealsFromServer();
},
loadDealsFromServer: function() {
var newDeals = [];
chrome.runtime.sendMessage({ action: "findDeals", personId: this.props.person.id }, function(deals) {
newDeals = deals;
});
this.setState({ deals: newDeals });
},
render: function() {
var dealNodes = this.state.deals.map(function(deal, index) {
return (
<Deal deal={deal} key={index} />
);
});
return (
<div className="deals">
<table>
<thead>
<tr>
<td>Name</td>
<td>Amount</td>
<td>Stage</td>
<td>Probability</td>
<td>Status</td>
<td>Exp. Close</td>
</tr>
</thead>
<tbody>
{dealNodes}
</tbody>
</table>
</div>
);
}
});
However, if I add a debugger like below, newDeals are populated, and then once I continue, i see the data:
loadDealsFromServer: function() {
var newDeals = [];
chrome.runtime.sendMessage({ action: "findDeals", personId: this.props.person.id }, function(deals) {
newDeals = deals;
});
debugger
this.setState({ deals: newDeals });
},
This is what's calling deals list:
var Gmail = React.createClass({
render: function() {
return (
<div className="main">
<div className="panel">
<DealsList person={this.props.person} />
</div>
</div>
);
}
});
I'd like to add to this the enormously simple, but oh so easily made mistake of writing:
this.state.something = 'changed';
... and then not understanding why it's not rendering and Googling and coming on this page, only to realize that you should have written:
this.setState({something: 'changed'});
React only triggers a re-render if you use setState to update the state.
My scenario was a little different. And I think that many newbies like me would be stumped - so sharing here.
My state variable is an array of JSON objects being managed with useState as below:
const [toCompare, setToCompare] = useState([]);
However when update the toCompare with setToCompare as in the below function - the re-render won't fire. And moving it to a different component didn't work either. Only when some other event would fire re-render - did the updated list show up.
const addUniversityToCompare = async(chiptoadd) =>
{
var currentToCompare = toCompare;
currentToCompare.push(chiptoadd);
setToCompare(currentToCompare);
}
This was the solution for me. Basically - assigning the array was copying the reference - and react wouldn't see that as a change - since the ref to the array isn't being changed - only content within it. So in the below code - just copied the array using slice - without any change - and assigned it back after mods. Works perfectly fine.
const addUniversityToCompare = async (chiptoadd) => {
var currentToCompare = toCompare.slice();
currentToCompare.push(chiptoadd);
setToCompare(currentToCompare);
}
Hope it helps someone like me. Anybody, please let me know if you feel I am wrong - or there is some other approach.
Thanks in advance.
That's because the response from chrome.runtime.sendMessage is asynchronous; here's the order of operations:
var newDeals = [];
// (1) first chrome.runtime.sendMessage is called, and *registers a callback*
// so that when the data comes back *in the future*
// the function will be called
chrome.runtime.sendMessage({...}, function(deals) {
// (3) sometime in the future, this function runs,
// but it's too late
newDeals = deals;
});
// (2) this is called immediately, `newDeals` is an empty array
this.setState({ deals: newDeals });
When you pause the script with the debugger, you're giving the extension time to call the callback; by the time you continue, the data has arrived and it appears to work.
To fix, you want to do the setState call after the data comes back from the Chrome extension:
var newDeals = [];
// (1) first chrome.runtime.sendMessage is called, and *registers a callback*
// so that when the data comes back *in the future*
// the function will be called
chrome.runtime.sendMessage({...}, function(deals) {
// (2) sometime in the future, this function runs
newDeals = deals;
// (3) now you can call `setState` with the data
this.setState({ deals: newDeals });
}.bind(this)); // Don't forget to bind(this) (or use an arrow function)
[Edit]
If this doesn't work for you, check out the other answers on this question, which explain other reasons your component might not be updating.
Another oh-so-easy mistake, which was the source of the problem for me: I’d written my own shouldComponentUpdate method, which didn’t check the new state change I’d added.
To update properly the state, you shouldn't mutate the array. You need to create a copy of the array and then set the state with the copied array.
const [deals, setDeals] = useState([]);
function updateDeals(deal) {
const newDeals = [...deals]; // spreading operator which doesn't mutate the array and returns new array
newDeals.push(deal);
// const newDeals = deals.concat(deal); // concat merges the passed value to the array and return a new array
// const newDeals = [...deals, deal] // directly passing the new value and we don't need to use push
setDeals(newDeals);
}
In my case, I was calling this.setState({}) correctly, but I my function wasn't bound to this, so it wasn't working. Adding .bind(this) to the function call or doing this.foo = this.foo.bind(this) in the constructor fixed it.
My issue was that I was using 'React.PureComponent' when I should have been using 'React.Component'.
I was updating and returning the same object passed to my reducer. I fixed this by making a copy of the element just before returning the state object like this.
Object.assign({}, state)
I was going through same issue in React-Native where API response & reject weren't updating states
apiCall().then(function(resp) {
this.setState({data: resp}) // wasn't updating
}
I solved the problem by changing function with the arrow function
apiCall().then((resp) => {
this.setState({data: resp}) // rendering the view as expected
}
For me, it was a binding issue. Using arrow functions solved it because arrow function doesn't create its's own this, its always bounded to its outer context where it comes from
After looking into many answers (most of them are correct for their scenarios) and none of them fix my problem I realized that my case is a bit different:
In my weird scenario my component was being rendered inside the state and therefore couldn't be updated.
Below is a simple example:
constructor() {
this.myMethod = this.myMethod.bind(this);
this.changeTitle = this.changeTitle.bind(this);
this.myMethod();
}
changeTitle() {
this.setState({title: 'I will never get updated!!'});
}
myMethod() {
this.setState({body: <div>{this.state.title}</div>});
}
render() {
return <>
{this.state.body}
<Button onclick={() => this.changeTitle()}>Change Title!</Button>
</>
}
After refactoring the code to not render the body from state it worked fine :)
If someone is here for similar problem, but using React Functional components rather class components AND also using react reducer, --> Move your api call outside of the reducer. Reducer should never do an api call. Refer to https://stackoverflow.com/a/39516485/12121297 for detailed response
In my case the issue was related to a child component.
In a nutshell:
Parent component updates the state of an array of objects and
forwards the updated array to a child component.
The child component receives the array, creates a copy of if (spread
operator) and uses it to update an internal stateful component which
is supposed to be rendered.
However, react does not re-render the child component when the update
occurs in the parent component.
I solved my problem by adding a useEffect hook in the child components.
I still don’t really understand how the state update is handled in my case.
The code is available in my stackblitz repo here:
https://stackblitz.com/edit/react-hel9yv?file=src%2FApp.js

Categories

Resources