Why is my array undefined when Vue component is created? - javascript

I'm having trouble understanding why an array is undefined in my Vue component. Here is the flow:
SelectCategories.vue component uses router to push to Quiz.vue component. Here I use props to pass on data to Quiz component. Here is how it is done:
else {
this.$router.push({
name: 'quiz',
params: {
items: selectedCategories
}
})
}
We move on to Quiz.vue component, where I do the following on the created life cycle hook:
async created() {
for (const item of this.items) {
var questions = await QuestionService.getQuestionsByCategory(item);
this.questions.push(questions);
console.log(this.questions);
}
}
The QuestionService reaches out to the database and gets some information, which is pushed into the questions array I have defined here:
export default {
name: 'quiz',
props: ['items'],
data: function() {
return {
questions: [],
error: '',
};
}
Finally, I try to access this data in the template using an h1:
<h1>{{questions[0][0].description}}</h1>
However, I end up with the following error:
TypeError: Cannot read property '0' of undefined
What I am seeing in the console is that:
{{questions[0][0].description}}
Is happening before I populate the questions array in the created life cycle hook. As you can see here:
It is my understanding that created is called before the template, but I might be wrong. What I want to accomplish is to have the functionality in created() have happen before the template is loaded, so the array gets populated first. Thank you for the help.

What I want to accomplish is to have the functionality in created() have happen before the template is loaded
.. this is not possible. Read more on JS asynchrony. await does not block the execution of whole app. created() hook just returns promise and is scheduled to continue when async call finishes.
Only way is to write your template in a way that the data are not present at first and will come later...

Making created async and using await inside it does not prevent the component from being created and mounted. It only prevents the execution of code placed in created after await, until the promise resolves.
Which means your component will render without the expected contents in questions (until questions get updated, after the call returns).
The solution is to prevent anything currently breaking inside your component from breaking by wrapping those blocks in if conditions (or v-if in <template>).
A common solution for this type of problem is to make the call from outside the component and condition its rendering (creation) based on the response. Typically you want to place data that's consumed by more than one component in a store (single source of truth) which can then be injected anywhere you need it.
For more complex stores you could use Vuex and for very simple ones (basic data sharing) you could use Vue.observable().

Related

Vuex/Redux store pattern - sharing single source of data in parent and child components that require variations of that data

I understand the benefits of using a store pattern and having a single source of truth for data shared across components in an application, and making API calls in a store action that gets called by components rather than making separate requests in every component that requires the data.
It's my understanding that if this data needs to change in some way, depending on the component using the data, this data can be updated by calling a store action with the appropriate filters/args, and updating the global store var accordingly.
However, I am struggling to understand how to solve the issue whereby a parent component requires one version of this data, and a child of that component requires another.
Consider the following example:
In an API, there exists a GET method on an endpoint to return all people. A flag can be passed to return people who are off sick:
GET: api/people returns ['John Smith', 'Joe Bloggs', 'Jane Doe']
GET: api/people?isOffSick=true returns ['Jane Doe']
A parent component in the front end application requires the unfiltered data, but a child component requires the filtered data. For arguments sake, the API does not return the isOffSick boolean in the response, so 2 separate requests need to be made.
Consider the following example in Vue.js:
// store.js
export const store = createStore({
state: {
people: []
},
actions: {
fetchPeople(filters) {
// ...
const res = api.get('/people' + queryString);
commit('setPeople', res.data);
}
},
mutations: {
setPeople(state, people) {
state.people = people;
}
}
});
// parent.vue - requires ALL people (NO filters/args passed to API)
export default {
mounted() {
this.setPeople();
},
computed: {
...mapState([
'people'
])
},
methods: {
...mapActions(['setPeople']),
}
}
// child.vue - requires only people who are off sick (filters/args passed to API)
export default {
mounted() {
this.setPeople({ isOffSick: true });
},
computed: {
...mapState([
'people'
])
},
methods: {
...mapActions(['setPeople']),
}
}
The parent component sets the store var with the data it requires, and then the child overwrites that store var with the data it requires.
Obviously the shared store var is not compatible with both components.
What is the preferred solution to this problem for a store pattern? Storing separate state inside the child component seems to violate the single source of truth for the data, which is partly the reason for using a store pattern in the first place.
Edit:
My question is pertaining to the architecture of the store pattern, rather than asking for a solution to this specific example. I appreciate that the API response in this example does not provide enough information to filter the global store of people, i.e. using a getter, for use in the child component.
What I am asking is: where is an appropriate place to store this second set of people if I wanted to stay true to a store focused design pattern?
It seems wrong somehow to create another store variable to hold the data just for the child component, yet it also seems counter-intuitive to store the second set of data in the child component's state, as that would not be in line with a store pattern approach and keeping components "dumb".
If there were numerous places that required variations on the people data that could only be created by a separate API call, there would either be a) lots of store variables for each "variation" of the data, or b) separate API calls and state in each of these components.
Thanks to tao I've found what I'm looking for:
The best approach would be to return the isOffSick property in the API response, then filtering the single list of people (e.g. using a store getter), thus having a single source of truth for all people in the store and preventing the need for another API request.
If that was not possible, it would make sense to add a secondary store variable for isOffSick people, to be consumed by the child component.

Where to act on changed child properties in React JS

I'm confused, and I have searched a lot for the answer to this (seemingly) basic question.
I'm learning React, and I have a rather common component hierarchy with one top component (lets call it App) which contains a number of subcomponents (a grid, a graph, a table etc).
They all show information regarding one product.
Now when I select a row in the grid, I want to inform the other subcomponents about the change. App therefore passes a callback method
onSelectedProduct={this.onSelectedProduct}
to the grid. This gets called OK. In this App method I set the state:
onSelectedProduct(product) {
this.setState({ product: product });
}
In its render(), App has declared another subcomponent:
<ProductGraph product={this.state.product} />
Since ProductGraph needs to fetch some data asynchronously "to-be-rendered" later, where should I catch this property change??
The old "componentWillReceiveProps" sounded like the proper place, but will be deprecated and should not be used, I understand.
I have also tried shouldComponentUpdate, getDerivedStateFromProps and even to catch it in render, but they all have downsides and eventually lead to horrible code.
Somewhere, somehow, I should be able to detect that props.product !== state.product and issue an async load call for the data...
When the async method I call returns with the data, it will set the state and render itself.
So where is the optimal place to catch changed properties?
I have read a lot about the React Lifecycle but I just can't seem to find this basic information. Am I stupid or maybe blind? Or have I got this completely wrong somehow?
You are looking for componentDidUpdate() the lifecycle method that triggers when a component receives new props or has an updated state.
https://reactjs.org/docs/react-component.html#componentdidupdate
In your ProductGraph component, you would do something like:
componentDidUpdate(prevProps){
if(this.props.product !== prevProps.product){
fetch(`myApi/${this.props.product}`)
.then(res => res.json())
.then(data => this.setState({ data: data })) <-- identify what you need from object
.catch((errors) => {
console.log(errors)
})
}
}

Vue reactive data showing as Observer and not actual value

I have some methods in a component:
created() {
return this.getConversation(this.$route.params.ConversationId).then(
respConversation => {
this.conversation = respConversation;
}
);
},
mounted() {
console.log(this.conversation);
return this.getConversationTranscripts(this.conversation.AudioId);
},
However, this.conversation prints as {__ob__: Observer} and doesn't have AudioId
Both this.getConversation and this.getConversationTranscripts return promises.
Returning Promises from Vue's lifecycle hooks doesn't do much. It certainly doesn't make them wait for the Promise to complete. The same would be true using async/await.
Internally the hooks are called using callHook, e.g. callHook(vm, 'mounted'). You can see the code for callHook at:
https://github.com/vuejs/vue/blob/b7c2d9366cf731a1551286b8ac712e6e0905070e/src/core/instance/lifecycle.js#L336
This calls out to invokeWithErrorHandling, which you can see at:
https://github.com/vuejs/vue/blob/b7c2d9366cf731a1551286b8ac712e6e0905070e/src/core/util/error.js#L36
While invokeWithErrorHandling does have some minimal support for handling any Promises that are returned, those Promises are ignored by callHook.
In the code shown in the question the created hook will be called followed by the mounted hook. The then on the getConversation Promise won't be called until later as that will be asynchronous.
So at the point mounted is called the value of this.conversation will still be its initial value, presumably an empty object. Vue's reactivity system will cause that to be shown as {__ob__: Observer} in the console.
If the component needs that data to be able to function then the parent will have to take responsibility for loading that data and delay creating the component until the data is available.
More likely the component will just need to cope with the data being missing when it's first rendered. It will then need to chain together the asynchronous calls using Promises:
created() {
this.getConversation(this.$route.params.ConversationId).then(
respConversation => {
this.conversation = respConversation;
this.getConversationTranscripts(this.conversation.AudioId);
}
);
}
Using async/await is maybe a little cleaner here:
async created() {
this.conversation = await this.getConversation(this.$route.params.ConversationId)
this.getConversationTranscripts(this.conversation.AudioId);
}
None of this will pause the component's lifecycle, so rendering will continue with this.conversation still set to its initial value and the template will have to be coded to deal with that.
I would add that using this.$route.params.ConversationId is not ideal. If possible this would be injected via a prop instead. You'll also need to be very careful when this value changes. If the value changing causes a new component to be created then no problem but if it just updates the existing component you could run into problems. The created and mounted hooks only run once, when the component is first created, so reusing the same component when the params change won't run them again.

Parsing JSON objects array and displaying it in ReactJS

I've been facing a weird issue lately with my React App. I'm trying to parse a JSON object that contains arrays with data. The data is something like this:
{"Place":"San Francisco","Country":"USA", "Author":{"Name":"xyz", "Title":"View from the stars"}, "Year":"2018", "Places":[{"Price":"Free", "Address":"sfo"},{"Price":"$10","Address":"museum"}] }
The data contains multiple arrays like the Author example I've just shown. I have a function that fetches this data from a URL. I'm calling that function in componentDidMount. The function takes the data i.e responseJson and then stores it in an empty array that I've set called result using setState. In my state I have result as result:[]. My code for this would look something like this:
this.setState({result:responseJson})
Now, when I've been trying to access say Author Name from result I get an error. So something like this:
{this.state.result.Author.Name}
I'm doing this in a function that I'm using to display stuff. I'm calling this function in my return of my render function. I get an error stating :
TypeError:Cannot read property 'Name' of undefined. I get the same error if I try for anything that goes a level below inside. If I display {this.state.result.Place} or {this.state.result.Country} it's all good. But if I try,say {this.state.result.Author.Title} or {this.state.result.Places[0].Price} it gives me the same error.
Surprising thing is I've parsed this same object in a different component of mine and got no errors there. Could anyone please explain me why this is happening?
If I store the individual element while I setState in my fetch call function, I can display it. For example:
{result:responseJson,
AuthorName:responseJson.Author.Name
}
Then I'm able to go ahead and use it as {this.state.AuthorName}.
Please help me find a solution to this problem. Thanks in advance!
It could be that your state object is empty on the first render, and only updated with the data from the API after the request has completed (i.e. after the first render). The Name and Place properties don't throw an error, as they probably resolve to undefined.
Try putting an if block in your render method to check if the results have been loaded, and display a loading indicator if they haven't.
I'm guessing your initial state is something like this:
{ results: {} }
It's difficult to say without seeing more code.
[EDIT]: adding notes from chat
Data isn't available on first render. The sequence of events rendering this component looks something like this:
Instantiate component, the initial state is set to { results: [] }
Component is mounted, API call is triggered (note, this asynchronous, and doesn't return data yet)
Render method is called for the 1st time. This happens BEFORE the data is returned from the API request, so the state object is still {results: [] }. Any attempts to get authors at this point will throw an error as results.Authors is undefined
API request returns data, setState call updates state to { results: { name: 'test', author: [...] } }. This will trigger a re-render of the component
Render method is called for the 2nd time. Only at this point do you have data in the state object.
If this state evolves, means it is changed at componentDidMount, or after a fetch or whatever, chances are that your state is first empty, then it fills with your data.
So the reason you are getting this error, is simply that react tries to get this.state.result.Author.Name before this.state.result.Author even exists.
To get it, first test this.state.result.Author, and if indeed there's something there, then get Author.Name like this.
render(){
return(
<div>
{this.state.result.Author ? this.state.result.Author.Name : 'not ready yet'}
</div>
);
}
[EDIT] I'll answer the comment here:
It's just because they are at a higher level in the object.
this.state.result will always return something, even false if there is no result key in your state (no result key in your constructor for instance when the component mounts).
this.state.result.Country will show the same error if result is not a key of your state in your constructor. However, if result is defined in your constructor, then it will be false at first, then become the data when the new state populates.
this.state.result.Author.Name is again one level deeper...
So to avoid it, you would have to define your whole "schema" in the constructor (bad practice in my opinion). This below would throw no error when getting this.state.result.Author.Name if I'm not mistaken. It would first return false, then the value when available.
constructor(props){
super(props);
this.state={
result: {
Author: {}
}
}
}

vue.js - dynamically generated component in v-for does not update bound property correctly

I came upon a problem in vue.js and I'm not sure how I should solve it. I'll try to explain the gist of it as clear and short as possible.
Situation
I load data from a rest API. I created a class using the javascript prototype syntax to create a class called DataLoader which handles all the loading and internal state changes. More often than not, the loaded data is displayed in a repetitive way (e.g. list or cards). I created a new component called DataLoaderWrapper for this purpose, taking a DataLoader object as property. The DataLoaderWrapper displays loading spinners according to the loading state of the DataLoader object. In addition, the component markup includes the following tag:
<slot :dataLoader='dataLoader' />
.. to allow for easy access to the loaded data. The idea is to use the component like so:
<DataLoaderWrapper :dataLoader="myDataLoader">
<template slot-scope="props">
<template v-for="item in props.dataLoader.data">
{{item}}
</template>
</template>
</DataLoaderWrapper>
Now just to clear this up right away; why do I use props.dataLoader instead of myDataLoader within the template? The basic idea is that the DataLoader object sometimes is generated dynamically and therefore the most direct access to the object instance is via the component bound property. (I also tried direct access to the loader and found no different behavior, but this way makes the component easier to use for me)
Now let's extend this basic concept.
Imagine you have a DataLoader object which is configured to load a list of music releases. This would look something like this:
<DataLoaderWrapper :dataLoader="myReleaseLoader">
...
</DataLoaderWrapper>
Furthermore, every release is published by one or more artists. The list of artists needs to be loaded separately. I can extend the example like this:
<DataLoaderWrapper :dataLoader="myReleaseLoader">
<template slot-scope="props">
<template v-for="release in props.dataLoader.data">
<h1>release.Title</h1>
Artists:
<DataLoaderWrapper :dataLoader="getArtistsLoader(release)">
<template slot-scope="nestedProps">
{{nestedProps.dataLoader.data.map(artist => artist.Name).join(', ')}}
</template>
</DataLoaderWrapper>
</template>
</template>
</DataLoaderWrapper>
The Problem
The releases are loaded correctly and the templates of myReleaseLoader are rendered correctly. As soon as this happens, a new DataLoader instance is created dynamically and ordered to load the artists for a given release. This happens for every release. While the artist loaders are loading, the loading spinner is displayed. The problem is, that the nestedProps variable does not get updated as soon as the DataLoader instance is done loading the data.
I've been trying to solve this issue for at least two days, meaning I've debugged and checked everything that came to my mind.
I am sure that the data is correctly loaded and existent. But the loading spinner of the DataLoaderWrapper component is still displayed and printing nestedProps.dataLoader reveals data: [] which does not line up with the state shown when inspecting the component. This brings me to the conclusion, that the nestedProps property is not updated correctly.
I think this could be because vue bindings are one-directional and an update in a child component will not trigger the parent to update.
Having the page loaded on my local machine with 'hot-reloading' on performs an update to the UI every time I save changes in my files without performing a full reload of the page. The loaders are in a different state when updating the UI now than they are when loading the page for the first time. The data does not get loaded again, because the getArtistsLoader(..) function implements a cache which guarantees that always the same loader is returned for a given release when requested, this means that the data is essentially already existent when re-rendering the page, resulting in the desired output being displayed. I wasn't able to get into the correct state otherwise.
I tried to work with computed properties, direct access to the loader and more direct access to the desired property (e.g. props.data instead of props.dataLoader.data), none of which solved the update issue.
How can I do this?
The problem is getArtistsLoader does not return data synchronously
const data = getArtistsLoader(release);
// data === undefined
so inside the DataLoaderWrapper, it gets undefined for the dataLoader props
Solution
DataLoaderWrapper can accept a promise as data, so it can update when promise resolved.
1. update DataLoaderWrapper to accept promise
I use an async prop to distinguish dataLoader is asynchronous or not
<template>
<div>
<slot :dataLoader="realData"/>
</div>
</template>
<script>
export default {
props: {
dataLoader: null,
async: Boolean
},
data() {
let realData = this.dataLoader;
if (this.async) {
realData = [];
this.dataLoader
.then(data => {
this.realData = data;
})
.catch(error => {
console.log(error);
});
}
return {
realData: realData
};
}
};
</script>
2. make getArtistsLoader return promise
function getArtistsLoader(release) {
retrun axios.get('/artists?release=' + release.id)
.then((response) => response.data)
}
3. add async prop
<DataLoaderWrapper async :dataLoader="getArtistsLoader(release)">
full demo

Categories

Resources