chartjs in react useEffect keeps on creating new charts? - javascript

I have this extremely odd issue.. every single time I try and use a chartjs object, it keeps on creating itself on a new object (new id). The initial load never works, and I have to refresh. Everytime I switch from unmounting this component to mounting, when I console.log the chart instance it's always incrementing in id. I'm confused as what's going wrong here.
here is a basic snipped of my code:
useEffect(() => {
if (chartContainer && chartContainer.current) {
const newChartInstance = new Chartjs(chartContainer.current!, chartConfig);
removeData(newChartInstance);
if (newChartInstance) {
setIsClear(true);
setChartInstance(newChartInstance);
return () => {
removeData(newChartInstance);
newChartInstance.destroy();
if (chartInstance) chartInstance.destroy();
}
}
};
}, [chartContainer]);
useEffect(() => {
if (chartInstance && delay) {
const intervalUpdate = setInterval(() => {
console.log("updating");
console.log(chartInstance);
chartInstance.update();
}, delay);
return () => clearInterval(intervalUpdate);
}
}, [chartInstance, delay])
return (
<div className={styles.chartContainer}>
<canvas ref={chartContainer} height={"140"}/>
</div>
);
On the initial load a chart is created, and I'm adding data to it. But I believe 2nd one gets created and that's holding no data & is what is being display on the canvas. I know there's two somehow because because when I log I get an id of 1, instead of 0.
When I console.log() chart instance, on the very first initial load is always:
Chart {id: 1, ctx: CanvasRenderingContext2D, canvas: canvas.chartjs-render-monitor, config: {…}, width: 1024, …}
Which I believe is why my chart is empty. When I refresh it goes back to:
Chart {id: 0, ctx: CanvasRenderingContext2D, canvas: canvas.chartjs-render-monitor, config: {…}, width: 1024, …}
which properly displays the data.

Related

Map layers do not render again past initial load

I am using VueMapbox (0.4.1) to utilize Mapbox GL in a Vue project.
<template>
<MglMap
:accessToken="accessToken"
:mapStyle.sync="mapStyle"
:repaint="true"
#load="onMapLoaded">
<MglMarker
:coordinates="someCoordinates"
class="map-marker-wrapper">
<div
slot="marker"
class="map-marker">
</div>
</MglMarker>
</MglMap>
</template>
<script>
import Mapbox from 'mapbox-gl'
import { MglMap, MglMarker } from 'vue-mapbox'
import * as MAP from '#/constants/map'
// Vue-Mapbox documentation: https://soal.github.io/vue-mapbox/guide/basemap.html#adding-map-component
export default {
name: 'Map',
components: {
MglMap,
MglMarker
},
props: {
someCoordinates: {
type: Array,
required: true
},
darkMode: {
type: Boolean,
required: true
}
},
data () {
return {
accessToken: MAP.ACCESS_TOKEN,
mapbox: null,
map: null,
actionsDispatcher: null
}
},
computed: {
mapStyle () {
return this.darkMode ? MAP.COLOR_PROFILES.DARK : MAP.COLOR_PROFILES.LIGHT
}
},
created () {
this.mapbox = Mapbox
},
methods: {
async onMapLoaded (event) {
this.map = event.map
this.actionsDispatcher = event.component.actions
await this.actionsDispatcher.flyTo({
center: this.someCoordinates
})
}
}
}
</script>
On the first load, everything works as expected:
But if I move to a different pseudo-route (say from /#/Map to /#/Profile and back), some of the map layers specified by my mapStyle (roads, city names,..) are not rendered anymore (default layers instead). The map also stops honoring any change of the mapStyle url, even when I specify mapStyle.sync in my template.
If I hit the browser's reload button it loads the layers as expected as the entire app is reloaded from scratch, but I unfortunately cannot afford to do this by default.
Any ideas are greatly appreciated.
I found a solution, in the example of vue-mapbox, the variable map (this.map) is set by "event.map" which causes an error because the card is not refreshed afterwards.
In my case i just remove that (this.map = event.map) in my onMapLoaded function and this is great.
Have a good day.
Despite I don’t know the syntax of Vue.js, the problem you are facing is that you are creating your layers in map.on('load', ... which is an event that happens only once, so when the style change happens, all the layers of the map style (including the ones created by custom code) are removed.
If you want to recreate your layers on style change, you have to do it in the event map.on('style.load', ..., but as said, I don’t see in your vue.js code where that is being done. If you share the part of the code where vue.js is invoking the methods it’ll be easier to help you

Vue.js infinite loop on component re-render [closed]

Closed. This question is not reproducible or was caused by typos. It is not currently accepting answers.
This question was caused by a typo or a problem that can no longer be reproduced. While similar questions may be on-topic here, this one was resolved in a way less likely to help future readers.
Closed 2 years ago.
Improve this question
I'm trying to build tables based off a few selected properties from a previous component:
I'm rendering a component called 'branch-comparison' to compare XML files and their properties and values. This component takes in two props:
selectedEnvs: An array of objects with a name and object
commonFiles: An array of files with a name and object
I'm using vue-tables-2 to build these tables. At the top of the template it runs a function called getProps() to generate a set of all possible properties from each file. I've hard coded 0 because currently I'm only letting the user choose 1 file at a time. It then goes through each file (only 1) and gets data for the main table and the comparison tables. They are virtually the same function (getHeadData and getTableData) but I've seperated them for now for further customization. The code is not that important for actually generating the tables, however something inside of them is causing my code to go in an infinite loop.
On the initial render of the component, there is never an infinite loop. Everything runs through, and doesn't break at all and works wonderfully. Once however the component has been rendered, and I make a change to the props, or even simply save the file in the editor and vue-cli hot reloads it, it goes into and infinite loop. All the data still get's generate fine and the component does as it's supposed to. But it loops through 101 times no matter what.
Things I've looked into:
Changing the data: I fully understand a component re renders on data change... however I don't believe I am changing any reactive data in any method call. I'm simply declaring it locally inside the function and returning it to that temporary variable. Also if this was the case, I believe it would go into an infinite loop on the initial component load, but this is not the case. It goes into the infinite loop only on a refresh or prop change.
Mutating the Vuex state: I looked into this but I am never changing the state of anything. I am simply accessing it in the getTableData and getHeadData methods. I then thought, perhaps assigning a variable to point to this state object is causing it to re render based on something accessing the state, so I tried instead of
this.$store.state.branchesToCompare[branchIdx].obj[env.name].app_config[this.commonFiles[fileIdx]].forEach(envProp
=> {
to use
var x = JSON.parse(JSON.stringify(this.$store.state.branchesToCompare[branchIdx].obj[env.name].app_config[this.commonFiles[fileIdx]])
then
x.forEach(envProp =>
but this still does not work.
If I comment out the code that calls getHeadData() and getTableData() it loops through the appropriate amount of times.
Here is the code.. I am still new to Vue so any more general suggestions I am more than open to:
<template>
<div id="BranchComparison">
<div :set="info = getProps(0)">
<div class="file" v-for="(file, fileIdx) in commonFiles" :key="(file, fileIdx)">
<h3>{{ file }} </h3>
<b-row :set="mainTable = getHeadData(fileIdx, info.props, info.columns)">
<b-col class="mainBranch">
<h5 class="fileName"> {{ $store.state.branchSelection.split('.').slice(0, -1).join('.') }} <span style="font-size: 14px;">vs </span> </h5>
<v-client-table
:data="mainTable.data"
:columns="mainTable.columns"
:options="mainTableOptions"
size="small"
></v-client-table>
</b-col>
<b-col class="compareBranch" v-for="(branch, branchIdx) in $store.state.branchesToCompare" :key="(branch, branchIdx)">
<h5> {{ branch.name.split('.').slice(0, -1).join('.') }} </h5>
<v-client-table
:set="temp = getTableData(fileIdx, branchIdx, info.props, info.columns, mainTable)"
:data="temp.data"
:columns="temp.columns"
:options="temp.options"
size="small"
></v-client-table>
</b-col>
</b-row>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['selectedEnvs', 'commonFiles'],
data(){
return{
mainTableOptions:{
filterable: false,
filterByColumn: false,
perPage: 200,
pagination: {
show: false,
dropdown: false
},
sortable: [''],
resizableColumns: false,
},
}
},
methods: {
getTableData(fileIdx, branchIdx, props, columns, mainTable){
var data = []
var compareTableOptions = {
filterable: false,
perPage: 200,
pagination: {
show: false,
},
sortable: [''],
hiddenColumns: ['Property'],
resizableColumns: false,
cellClasses: {}
}
props.forEach(prop => {
var temp = { Property: prop }
this.selectedEnvs.forEach(env => {
var found = false;
this.$store.state.branchesToCompare[branchIdx].obj[env.name].app_config[this.commonFiles[fileIdx]].forEach(envProp => {
if(envProp){
if (prop == envProp["#name"]) {
compareTableOptions.cellClasses[env.name] = []
compareTableOptions.cellClasses[env.name].push({
class: 'same',
condition: row => {
try{
return row[env.name] == mainTable.data[i][env.name]
} catch{
console.log('This is a different problem ')
}
}
})
found = true;
temp[env.name] = envProp["#value"]
}
}
});
if (!found){
temp[env.name] = 'Not found'
}
})
data.push(temp)
});
return {
columns: columns,
data: data,
options: compareTableOptions
}
},
getHeadData(fileIdx, props, columns){
var data = []
props.forEach(prop => {
var temp = { Property: prop }
this.selectedEnvs.forEach(env => {
var found = false;
this.$store.state.jsonObject[env.name].app_config[this.commonFiles[fileIdx]].forEach(envProp => {
if(envProp){
if (prop == envProp["#name"]) {
found = true;
temp[env.name] = envProp["#value"]
}
}
});
if (!found){
temp[env.name] = 'Not found'
}
})
data.push(temp)
});
return {
columns: columns,
data: data
}
},
getProps(fileIdx){
if(this.commonFiles.length == 0) return
var columns = ['Property']
var props = new Set()
this.selectedEnvs.forEach((env, idx) => {
columns.push(env.name)
this.$store.state.branchesToCompare.forEach(branch => {
branch.obj[env.name].app_config[this.commonFiles[fileIdx]].forEach(prop => {
if(prop){
props.add(prop["#name"])
}
})
});
this.$store.state.jsonObject[env.name].app_config[this.commonFiles[fileIdx]].forEach(prop => {
if(prop){
props.add(prop["#name"]);
}
});
});
var ret = { props: props, columns: columns }
return ret;
}
}
}
</script>
I've solved it. The code above is actually fine. Shortly before I posted the code, I was using a computed property in the v-for AND in the getHeadData(), what I think was happening was it was a nested computed property, and on the inner loop it recomputed it and then tried the outer loop again, and so forth. I'm still puzzled why it work on the initial render, but oh well. It is working now.

Component rendered twice are sharing states and they shouldn't share them

The problem is that two views (same component) load same info (each one is a furnace, MainFurnaceView), an they should be loading different one. At first render (it can be seen for some miliseconds) they render different info, so it's okay, but later both views are showing the same info, after a second unwanted render. I think they're sharing the states between them.
I've got a MainFurnaceView element rendered twice in the MainScreen, the only property I give to it is furnace, on the first view, furnace={'forge'}, on the second one furnace={'preheat'}. Because of this property they should be loading different info.
On the MainFurnaceView, it logs the furnace prop correctly in the constructor, and also takes well info from AsyncStorage (called on constructor also). On render, at first, each view logs info of the wanted furnace, but later it loads and logs info of the 2nd furnace on both views.
Code:
MainScreen
<MainFurnaceView
//Tried with and without both onRef
//onRef={(ref) => (this.Option = ref)}
furnace={"forge"}/>
<MainFurnaceView
//onRef={(ref) => (this.Option = ref)}
furnace={"preheat"}/>
MainFurnaceView
constructor(props) {
super(props);
console.log('Constructor:', this.props.furnace);//Logs well
this.state = {
//Pieces to use at furnace view, tried with Object.assign and directly without it
furnacePieces:
this.props.furnace === "forge"
? Object.assign(pieces.forge, {})
: Object.assign(pieces.preheat, {}),
};
this.getData()/*Call to AsyncStorage here*/;}
getData = async () => {
//Get data (data) from key and save it as state
try {
let furnace;
if (this.props.furnace === "forge")
furnace = await AsyncStorage.getItem(consts.KEY_FORGE_DIMENSIONS);
else furnace = await AsyncStorage.getItem(consts.KEY_PREHEAT_DIMENSIONS);
furnace = JSON.parse(furnace);
console.log("getData: ", this.props.furnace, furnace);
if (typeof furnace[0].r === "number") {
this.setState({
furnaceRows: furnace[0].r,
furnaceCols: furnace[0].c,
furnaceHeights: furnace[0].h,
});
} else {
console.warn("not found");
this.setState({
furnaceRows: 3,
furnaceCols: 3,
furnaceHeights: 1,
});
}
} catch (e) {
// error reading value
console.log(e);
}
};
I call console.log('Render: ', this.props.furnace) on the render and there I also log the property furnace.
Log:
Constructor: forge
Render: forge
Constructor: preheat
Render: preheat
getData: forge Array [
Object {
"c": 4,
"h": 1,
"r": 6,
},
]
Render: preheat
getData: preheat Array [
Object {
"c": 4,
"h": 1,
"r": 4,
},
]
Render: preheat
What I understand with this log is that the first view is created and rendered, and then the second view is created and rendered, but later both reload with the property furnace as 'preheat' instead of one with 'forge' and the other with 'preheat'.
UPDATE:
If I don't call getData on constructor, views aren't re-rendered, so the info for each furnace is taken well. The problem now is that I need to get those dimensions from AsyncStorage, and idk how to do it without, as getting that info makes a new render and the prop furnace is changed.
BUT
If I make anything to re-render the mainScreen with both mainFurnaceView's (for example open another modal and close it) it renders on both views the 'preheat' info.

Vue reactive array data outputs different values

Im quite new to the vue reactive data workflow, and I'm attempting to make a image uploader, and its mainly working. Im trying to remove all true primary values from the images in the array, before updating a specific one. But I need to make a check on the "primary" values in the image array so i can make the conditional change.
I have left the image creation function at the bottom so you can see how this is made. the parent component gets all the data from server via ajax if being used to edit images this is the _images prop.
For some reason when i reference the array as images[key].primary, I get a different value than if I output the whole array in the console. Any ideas whats happening here?
Goal
Currently on the child component im clicking a make primary button and emitting the update to this component. Then in the update Method attempting to check if request is a new primary image request and make primary false on all other images. Then setting the requested image as primary. So a primary on toggle so there is only ever one primary image.
currently Im testing two images, in turn setting each one as primary true, and watching the other automatically turn false, but by the third click they are both true and nothing happens. Any ideas whats happening here?
Parent Component (v-image-upload):
export default {
components: {
'v-image': image
},
props: ['_errors', '_images', '_isMultiple'],
data() {
return {
images: _.cloneDeep(this._images),
}
},
methods: {
updateImage(request) {
//ISSUE: current images = true,true | images = false,true
console.log('current images', this.images[0].primary, this.images[1].primary, );
console.log('images', this.images);
if (request.form.primary && !this.images[request.key].primary) {
//reset all primary images
$.each(this.images, (index, value) => {
let image = value;
image.primary = false;
this.$set(this.images, index, image);
}
//update images with new image
this.$set(this.images, request.key, request.form);
},
onFileChange(e) {
let files = e.target.files || e.dataTransfer.files;
if (files.length) {
$.each(files, (index, file) => {
this.createImage(file);
});
}
e.target.value = '';
},
createImage(file) {
let reader = new FileReader();
let fileNameSegments = file.name.split('.');
let fileName = fileNameSegments[0];
let fileExtension = fileNameSegments[1];
let validExtnesions = ['jpg', 'png', 'gif'];
let placeholderImage = window.location.protocol + '//' + window.location.host + '/' + 'img/site/default/placeholder-image.png';
let primary = (this.images.length == 0) ? true : false;
reader.onload = (e) => {
let image = ($.inArray(fileExtension, validExtnesions) == -1) ? placeholderImage : e.target.result;
let data = {
name: fileName,
extension: fileExtension,
source: image,
description: null,
primary: primary,
image_directory: null,
};
this.$set(this.images, this.images.length, data);
}
reader.readAsDataURL(file);
},
}
}
Child Compononet (v-image):
export default {
props: ['_imgKey', '_image', '_errors'],
data() {
return {
form: {
id: _.clone(this._image.id),
name: _.clone(this._image.name),
description: _.clone(this._image.description),
primary: _.clone(this._image.primary),
extension: _.clone(this._image.extension),
image_directory: _.clone(this._image.image_directory),
source: _.clone(this._image.source),
},
}
},
watch: {
_image: {
handler(value) {
console.log('_image watch', this._imgKey, value.primary);
this.form.id = _.clone(value.id);
this.form.name = _.clone(value.name);
this.form.primary = _.clone(value.primary);
this.form.source = _.clone(value.source);
this.form.image_directory = _.clone(value.image_directory);
}, deep: true
}
},
methods: {
updatePrimary() {
this.form.primary = true;
this.updateImage();
},
updateImage() {
this.$emit('updateImage', {key: this._imgKey, form: this.form});
},
removeImage() {
this.$emit('removeImage', this._imgKey);
}
}
}
Update
I have been watching the vue developer tools for any changes to the image array. I push the primary button on the false image in the component this emits to the parent and calls the updateImage method. The first and second reset and update works, and changes the values. The issue has been tracked to: The 2nd 'after update' (true,false) does not match the third 'before reset'(true,true).
I made the previous changes mensioned in the comments and added a clone deep to both components data to prevent odd reactivity behaviour.
I checked the event log: 3x UpdateImage $emit by <VImage> which was expected.
Note: I moved the previous reset logic into the resetPrimary() method.
updateImage(request) {
console.log('update', request.key, request.form.primary);
console.log('b4 reset', this.images[0].primary, this.images[1].primary);
this.resetPrimary(request);
console.log('after reset', this.images[0].primary, this.images[1].primary);
this.$set(this.images, request.key, request.form);
console.log('after update', this.images[0].primary, this.images[1].primary);
},
output:
//First click - Image 1 (Expected)
update 1 true
b4 reset true false
after reset false false
after update false true
//Second click - Image 0 (Expected)
update 0 true
b4 reset false true
after reset false false
after update true false
//Third click - Image 1 (What?)
update 1 true
b4 reset true true //should be true false
after reset true true
after update true true
It appears that when attempting to update the same image a second time it somehow ignores the fact it has previously been reset to false.

Working with Vuex with Vue and displaying data

This is kind of a long explanation of an issue that I'm having on a personal project. Basically, I want to set a data property before my page loads when I read in data from a CSV file using D3.JS. I almost have it done but running into a small issue. Please read on to get more detail.
Basically, when the user comes to a page in my application, I want to display weather graphs. Like I said, I'm using D3.js to read in the data and created an action to do that. It works perfectly fine-I can console.log the data and I know its been read. However, in my vue instance I have a data property, which would hold the data set like this:
data() {
return {
name: this.$store.state.name
weatherData: this.$store.state.yearData
}
}
I then want to ensure that the weatherData is filled, with data from the csv file so I display it on the page like this:
<p>{{ weatherData }}</p>
Nothing special here. When the page loads, weatherData is blank. But I have a beforeMount life cycle hook and if I comment out the only line in it then it will display the data. If I then refresh the page, fire the action to get the data and then uncomment out the line in the beforeMount hook then the data appears! So before I continue this is my full code for the store:
export const store = new Vuex.Store({
state: {
name: 'Weather Data'
yearData: []
},
getters: {
},
mutations: {
setYearData(state, data) {
state.yearData = data
}
},
actions: {
getYearData: ({commit}) => {
d3.csv("../src/components/data/alaska.csv")
.then(function(data){
let yearData = []
for (let i = 0; i < data.length; i++){
let day = data[i].AKST
yearData.push(day)
}
//console.log(yearData)
commit('setYearData', yearData)
})
}
})
Here are parts of the vue file: The template:
<p>{{ weatherData }}</p>
The Vue Intance:
export default {
name: 'Weather',
data() {
return {
name: this.$store.state.name,
weatherData: this.$store.state.yearData
}
},
methods: {
...mapActions([
'getYearData'
])
},
beforeMount(){
this.$store.dispatch('getYearData') //(un)Commenting out this line will make my data appear
}
}
Page when it loads: Notice empty array:
Then either comment out or comment the one line in the beforeMount hook and get this: THE DATA!!!
Again, my end goal is to have the action called and the data set before the page finishes loading. Finally, I know that I don't need VUEX but this project is further helping me understand it. Any guidance on why this is happening would be great.
use mapState instead of putting your data in the data object, which sometimes being late on updating the template.
just make your Vue instance to look like:
import {mapState} from 'vuex'
export default {
name: 'Weather',
data() {
return { }
},
computed:{
...mapState({
name: state=>state.name,
weatherData: state=>state.yearData
})
},
methods: {
...mapActions([
'getYearData'
])
},
beforeMount(){
this.$store.dispatch('getYearData') //(un)Commenting out this line will make my data appear
}
thats way, you work directly with one source of truth-the store, and your name and weatherData will be reactive as well.
more about mapState here: https://vuex.vuejs.org/guide/state.html#the-mapstate-helper

Categories

Resources