Why array state change is not rendering again? - javascript

I was following the React learn section when i encountered this.
I´m drawing three shapes from an array called shapes. I modify the entire array when i press the button and the array do change when i do this.
import { useState } from "react";
export default function MyApp() {
const initialShapes = [
{ name: "circle", x: 50, y: 10, id: 1 },
{ name: "square", x: 100, y: 10, id: 2 },
{ name: "circle", x: 150, y: 10, id: 3 },
];
const [shapes, setShapes] = useState(initialShapes);
console.log(shapes);
return (
<>
<button
onClick={() => {
setShapes([{ name: "square", x: 20, y: 70, id: 2 }]);
}}
>
Just show one shape
</button>
{initialShapes.map((shape) => {
return (
<div
key={shape.id}
style={
shape.name === "circle"
? {
position: "absolute",
backgroundColor: "red",
width: "30px",
height: "30px",
borderRadius: "50px",
translate: `${shape.x}px ${shape.y}px`,
}
: {
position: "absolute",
backgroundColor: "red",
width: "30px",
height: "30px",
translate: `${shape.x}px ${shape.y}px`,
}
}
></div>
);
})}
</>
);
}
I can verify this by the console log at the beginning of the component. The state does uptade indeed but my visual shapes don't uptade with the new state.
const [shapes, setShapes] = useState(initialShapes);
console.log(shapes);
return (..
Console on first state
[
{
"name": "circle",
"x": 50,
"y": 10,
"id": 1
},
{
"name": "square",
"x": 100,
"y": 10,
"id": 2
},
{
"name": "circle",
"x": 150,
"y": 10,
"id": 3
}
]
Console on second state
[
{
"name": "square",
"x": 20,
"y": 70,
"id": 2
}
]
I know that if it was a mutation to the array, re-rendering won't be trigger but i'm actually setting a new state.
Why does this happen, react shouldn't notice the difference between array changes and trigger the render again ?

You are not using the shapes variable, you are mapping initialShapes.

Related

Reading all objects from multidimensional array

I want to read all objects from multidimensional array which looks like this:
const useMap = ref([
// ====================== ROW 1 ======================
[
{
x: 1,
y: 1,
currentPosition: false,
color: "white",
isWinning: false,
},
{
x: 16,
y: 1,
currentPosition: false,
color: "white",
isWinning: false,
},
],
// ====================== ROW 2 ======================
[
{
x: 1,
y: 2,
currentPosition: false,
color: "white",
isWinning: false,
},
{
x: 16,
y: 2,
currentPosition: false,
color: "white",
isWinning: false,
},
...
Already Im reading values by single rows like this:
<div v-for="tile in map[0]" :key:="tile" :tile="tile">
<div class="tile">{{ tile.y }} | {{ tile.x }}</div>
</div>
<div v-for="tile in map[1]" :key:="tile" :tile="tile">
<div class="tile">{{ tile.y }} | {{ tile.x }}</div>
</div>
...
Is there any option to read all rows once and not by single rows?
Nested arrays can be flattened with flat, and a computed is used to transform data to required shape before it's used:
const tiles = computed(() => useMap.value.flat());
Then it's used like intended:
<div v-for="tile in tiles" :key:="tile" :tile="tile">
You can achieve this requirement by converting the multi dimensional array into a single dimensional array using Array.flat() method and then iterate the values using v-for in the template.
Live Demo :
const { ref, reactive, readonly, computed } = Vue;
const App = {
setup () {
const useMap = ref([
[
{
x: 1,
y: 1,
currentPosition: false,
color: "white",
isWinning: false,
},
{
x: 16,
y: 1,
currentPosition: false,
color: "white",
isWinning: false,
}
],
[
{
x: 1,
y: 2,
currentPosition: false,
color: "white",
isWinning: false,
},
{
x: 16,
y: 2,
currentPosition: false,
color: "white",
isWinning: false,
}
]
])
const tiles = computed(() => useMap.value.flat());
return { tiles }
}
}
Vue.createApp(App).mount("#app");
<script src="https://unpkg.com/vue#next"></script>
<div id="app">
<div v-for="tile in tiles" :key:="tile" :tile="tile">
<div class="tile">{{ tile.y }} | {{ tile.x }}</div>
</div>
</div>

use JS Classes (CDN) with react (rewrite code), vanillaJS works

I would like to use JS classes from a cdn within a react component.
This is the cdn: https://cdn.jsdelivr.net/gh/Mikhus/canvas-gauges#gh-pages/download/2.1.7/all/gauge.min.js
I have already written the vanillaJS Code, that works well, however I have not idea how
to implement this with react.
The object that is drawn into a canvas is first instantiated with a set of different values,
then it is drawn on the canvas, then you can access the values from outside. I works well with vanillaJS!!
CDN or local file:
<script src="./lib/canvas-gauges.min.js"></script>
html canvas:
<canvas
id="barometer"
</canvas>
instantiating object and drawing it on the canvas:
const barometer = new RadialGauge({
renderTo: canvas,
title: 'hPa',
width: 400,
height: 400,
units: `${location}`,
minValue: 960,
maxValue: 1060,
majorTicks: [
"",
"970",
"980",
"990",
"1000",
"1010",
"1020",
"1030",
"1040",
"1050",
"",
],
minorTicks: 10,
ticksAngle: 270,
startAngle: 45,
strokeTicks: true,
highlights: [
{
"from": 960,
"to": 1005,
"color": "rgba(0,0,255,0.8)"
},
{
"from": 1005,
"to": 1023,
"color": "rgba(0, 151, 19, 0.3)"
},
{
"from": 1023,
"to": 1060,
"color": "rgba(241, 90, 34, 0.9)"
}
],
valueInt: 1,
valueDec: 0,
colorPlate: "beige",
colorMajorTicks: "#686868",
colorMinorTicks: "#686868",
colorTitle: "#000",
colorUnits: "#000",
colorNumbers: "#686868",
valueBox: true,
colorNeedleShadowUp: true,
colorNeedleShadowDown: true,
colorNeedle: "#f59042",
colorNeedleEnd: "#f59042",
colorNeedleCircleOuter: "rgba(200, 200, 200, 1)",
colorNeedleCircleOuterEnd: "rgba(200, 200, 200, 1)",
borderShadowWidth: 0,
borders: false,
borderInnerWidth: 0,
borderMiddleWidth: 0,
borderOuterWidth: 20,
colorBorderOuter: "silver",
colorBorderOuterEnd: "black",
needleType: "arrow",
needleWidth: 2,
needleCircleSize: 7,
needleCircleOuter: true,
needleCircleInner: false,
animationDuration: 1500,
animationRule: "dequint",
fontNumbers: "Arial",
fontTitle: "Arial",
fontUnits: "Arial",
fontValue: "Arial",
fontValueStyle: "italic",
fontNumbersSize: 16,
// fontNumbersStyle: "italic",
fontNumbersWeight: "bold",
fontTitleSize: 16,
fontUnitsSize: 16,
fontValueSize: 16,
animatedValue: true,
});
barometer.draw();
Then the values of that object can be accessed simply like this:
barometer.value = 1024;
Now, I would like to do the same thing with react!
There is also a node module called react-canvas-gauges, here I can make an import like this: import { RadialGauge, LinearGauge} from "react-canvas-gauges";
I can render then:
render() {
<RadialGauge
title= "hPa"
width = "400"... />
}
The point is, I figured out that I must update the value from "OUTSIDE", instantiating the object, drawing it on the canvas and then updating the value, and that is not possible (or against the philosophy of react) doing this with these JSX Components from "OUTSIDE"

How to write if else statement in reactJS when using useEffect?

Following is the code I'm using to return the Plotly graph, What I need to do in here is I need to return a message or different layout when the graph data is empty or there is nothing to show on the graph. use a default content like no data available if no data for charts. How do I do that?
import React, { useEffect } from "react";
import Plot from "react-plotly.js";
import PropTypes from "prop-types";
let dataArray;
let index;
let obj;
function PlotlyStackedChart({ labels, data, xTitle, yTitle, mainTitle, preHeading }) {
useEffect(() => {
dataArray = [];
for (index = 0; index < data.length; index += 1) {
obj = {
x: labels,
y: data[index].data,
type: "bar",
name: data[index].name,
};
dataArray.push(obj);
}
}, [data]);
return (
<>
<Plot
data={dataArray}
layout={{
barmode: "stack",
autosize: true,
title: {
text: preHeading + mainTitle,
y: 0.9,
},
margin: {
t: 225,
},
xaxis: {
// all "layout.xaxis" attributes: #layout-xaxis
title: {
text: xTitle,
font: {
family: "Arial Black",
},
}, // more about "layout.xaxis.title": #layout-xaxis-title
// dtick: 1,
},
yaxis: {
// all "layout.yaxis" attributes: #layout-yaxis
title: {
text: yTitle,
font: {
family: "Arial Black",
},
}, // more about "layout.yaxis.title": #layout-yaxis-title
},
font: {
// family: "Courier New, monospace",
size: 12,
color: "#7f7f7f",
},
legend: {
bgcolor: "transparent",
x: 0,
y: 1.4,
xanchor: "auto",
traceorder: "normal",
orientation: "h",
},
marker: { size: 40 },
}}
useResizeHandler
style={{ width: "100%", height: 600 }}
/>
</>
);
}
export default PlotlyStackedChart;
PlotlyStackedChart.defaultProps = {
xTitle: "",
yTitle: "",
mainTitle: "",
preHeading: "",
};
// Typechecking props for the MDDatePicker
PlotlyStackedChart.propTypes = {
labels: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).isRequired,
xTitle: PropTypes.string,
yTitle: PropTypes.string,
mainTitle: PropTypes.string,
preHeading: PropTypes.string,
data: PropTypes.oneOfType([PropTypes.string, PropTypes.array, PropTypes.object]).isRequired,
};
explaining further, If there is no data to plot in the graph I want to return the following code else the current one which retrieves the data and plot in the graph.
return {
"layout": {
"xaxis": {
"visible": false
},
"yaxis": {
"visible": false
},
"annotations": [
{
"text": "No matching data found",
"xref": "paper",
"yref": "paper",
"showarrow": false,
"font": {
"size": 28
}
}
]
}
}
You can try returning the empty content template if there is no content.
import React, { useEffect } from "react";
import Plot from "react-plotly.js";
import PropTypes from "prop-types";
let dataArray;
let index;
let obj;
function PlotlyStackedChart({ labels, data, xTitle, yTitle, mainTitle, preHeading }) {
useEffect(() => {
if(data?.length){ // <--- place condition here
dataArray = [];
for (index = 0; index < data.length; index += 1) {
obj = {
x: labels,
y: data[index].data,
type: "bar",
name: data[index].name,
};
dataArray.push(obj);
}
}
}, [data]);
// Place this conditional return
if(!data?.length) {
return <>No data found</>
}
return (
<>
<Plot
data={dataArray}
layout={{
barmode: "stack",
autosize: true,
title: {
text: preHeading + mainTitle,
y: 0.9,
},
margin: {
t: 225,
},
xaxis: {
// all "layout.xaxis" attributes: #layout-xaxis
title: {
text: xTitle,
font: {
family: "Arial Black",
},
}, // more about "layout.xaxis.title": #layout-xaxis-title
// dtick: 1,
},
yaxis: {
// all "layout.yaxis" attributes: #layout-yaxis
title: {
text: yTitle,
font: {
family: "Arial Black",
},
}, // more about "layout.yaxis.title": #layout-yaxis-title
},
font: {
// family: "Courier New, monospace",
size: 12,
color: "#7f7f7f",
},
legend: {
bgcolor: "transparent",
x: 0,
y: 1.4,
xanchor: "auto",
traceorder: "normal",
orientation: "h",
},
marker: { size: 40 },
}}
useResizeHandler
style={{ width: "100%", height: 600 }}
/>
</>
);
}
export default PlotlyStackedChart;
useEffect is entirely irrelevant to the problem you describe.
If you want to render something different when the array is empty, then do it in the render logic, not as an effect.
if (data.length === 0) {
return <Loading />
} else {
return <Plot .../>
}
That said, your effect logic is broken. You can't just assign a new array to a variable and have React render it. You've done nothing to tell React that it needs to do a re-render. Make data a state variable (with useState).

How to format the line chart moving with data and time in victory chart

For example, i have a sample chart like this
import {
VictoryChart,
VictoryGroup,
VictoryLine,
VictoryScatter,
VictoryTheme
} from "victory";
export default function App() {
return (
<div>
<VictoryChart>
<VictoryGroup
data={[
{ x: 1, y: 2 },
{ x: 1, y: 3 },
{ x: 3, y: 5 },
{ x: 4, y: 4 },
{ x: 5, y: 7 },
{ x: 6, y: 2 },
{ x: 7, y: 3 },
{ x: 8, y: 5 },
{ x: 9, y: 4 },
{ x: 10, y: 7 }
]}
color="blue"
>
<VictoryLine
style={{
data: { stroke: "#c43a31" },
parent: { border: "1px solid #ccc" }
}}
/>
<VictoryScatter size={6} symbol="star" />
</VictoryGroup>
</VictoryChart>
</div>
);
}
And it display like this
Problem is, my x data contain date and time, so if i convert to date, they will display at the same point like above chart
For example if x is 17/5/2021 11:11:11AM and another point is 17/5/2021 12:11:11AM ,when convert to date, it show the same point. I want to show the data also base on time, how can i do that, the format of date is something like this
[
"2021-05-17 14:32:27",
"2021-05-21 09:33:44",
"2021-05-26 09:05:47",
"2021-05-30 00:00:00",
"2021-06-04 10:27:53",
"2021-06-08 11:03:06",
"2021-06-12 07:25:37",
"2021-06-16 12:28:03",
"2021-06-21 12:44:47",
"2021-06-24 10:03:24",
"2021-06-28 08:22:59",
"2021-07-02 11:08:46",
"2021-07-09 14:28:26",
"2021-07-20 00:00:00",
"2021-07-31 00:00:00",
"2021-08-11 00:00:00",
"2021-08-22 00:00:00",
"2021-09-02 00:00:00",
"2021-09-13 00:00:00"
]
And i want it will be display something like this
As you can see, there are point between day 8 and 9, How can i do that, thank you a lots, here is the sample sandbox
https://codesandbox.io/s/charming-snow-yyc1h?file=/src/App.js:0-857

React.js handling 3rd party library mounting

I'm seeing an awkward bug using a third party library inside of a react component. I was able to reproduce a contrived demo for this post.
Let me start by explaining that I am using c3js - which is a charting library and rendering it in componentDidMount() and removing it in componentWillUnmount() with the correct calls to this.chart.destroy().
The bug occurs when filtering the components themselves, essentially what happens is the components are filtered correctly but the actual chart that sits inside the component stays the same as the first chart, which is very strange behaviour. Basically it's the wrong chart inside the wrong component!
You can see what I mean by clicking on the Remove all charts except chart 3 button, I have labeled the charts with a chartid and the filtering will correctly remove the other charts.
I am fairly certain it isn't my code because the filtering works correctly and updates the view. You can verify because I have labeled the charts and it is visible in the view. There is no console errors and I have verified my code works.
So my question is - can we work around this limitation using c3js, or is this really a problem with my code and the way I am rendering the charts.
Related demo: https://jsfiddle.net/69z2wepo/38614/
Related code:
var data = [
{
chartid: 1,
columns: [
['x', 0, 1, 2, 3, 4, 5, 6],
['data1', 130, 300, 330, 400, 300, 400, 500],
['data2', 390, 230, 200, 150, 100, 130, 210],
['data3', 290, 430, 300, 160, 210, 170, 190],
['data4', 190, 330, 200, 260, 190, 250, 320]
]
},
{
chartid: 2,
columns: [
['x', 0, 1, 2, 3, 4, 5, 6],
['data1', 130, 300, 330, 400, 300, 400, 500],
['data2', 390, 230, 200, 150, 100, 130, 210],
['data3', 290, 430, 300, 160, 210, 170, 190]
]
},
{
chartid: 3,
columns: [
['x', 0, 1, 2, 3, 4, 5, 6],
['data1', 130, 300, 330, 400, 300, 400, 500],
['data2', 390, 230, 200, 150, 100, 130, 210]
]
}
];
var Button = React.createClass({
handleDelete: function (id) {
this.props.handleDelete(id);
},
render: function() {
return (
<button onClick={this.handleDelete.bind(null, 3)}>
Remove all charts except chart 3
</button>
)
}
});
var Chart = React.createClass({
componentDidMount() {
this.chart = c3.generate({
bindto: '.chart-' + this.props.data.chartid,
data: {
columns: this.props.data.columns
}
});
},
componentWillUnmount() {
this.chart.destroy();
},
render: function() {
return (
<div>
<h4>{"chart-" + this.props.data.chartid}</h4>
<div className={"chart-" + this.props.data.chartid}>
</div>
</div>
)
}
});
var Child = React.createClass({
renderCharts: function(data) {
return data.map(function(metrics, i) {
return (
<Chart key={i} data={metrics} />
)
});
},
handleDelete: function(id) {
this.props.handleDelete(id);
},
render: function() {
return (
<div>
<Button handleDelete={this.handleDelete} />
{this.renderCharts(this.props.data)}
</div>
)
}
})
var App = React.createClass({
getInitialState: function() {
return {
initialData: this.props.data
}
},
handleDelete: function(id) {
var _filterFunc = function(data) {
if (data.chartid == id) return true;
return false;
};
var _filterCharts = Array.prototype.filter.call(this.state.initialData, _filterFunc);
this.setState({
initialData: _filterCharts
})
},
render: function() {
return (
<div>
<Child handleDelete={this.handleDelete} data={this.state.initialData} />
</div>
);
}
});
ReactDOM.render(
<App data={data} />,
document.getElementById('container')
);
The problem is the way you are setting the key on your chart. It's causing the renderer to get confused about which chart you're trying to keep.
Try this:
<Chart key={data[i].chartid} data={metrics} />
instead of <Chart key={i} data={metrics} />
Take a look at how React handles keys. Remember that you're uniquely identifying a child with a key for the lifecycle of the component. So since chart 1 is uniquely identified by key "1", you can't render chart 3 with key "1." My solution above ensures that the chart is uniquely identified by its chart id instead of by its rendering order.

Categories