React useEffect doesn't change data displayed on map - reactjs

I'm playing with uber's react-map-gl and deck-gl libraries to try to display data dynamically.I've got 2 components in my react little app.A navigation bar and a Mapbox map.So here is my pipeline.When the page first loads,only the map is displayed without any marker or visual data.But when i click on one of navigation bar link,an action creator gets called,when i make an ajax call to fetch some data,and from the action that is dispatched a pass my response data as payload and the reducer is reached so that i have a new version of the store.the data that i would like to visualize in they stor key : mainProjects that contains an array of geojson.My click on a navbar link succefully updates that value,and in the actual Map component,i can load the new values but useEffect does not update my localstate.Here is my map code:
import React, { useState, useEffect, useContext } from "react";
import { StaticMap } from "react-map-gl";
import { MapContext } from "./contexts/MapProvider";
import DeckGL, { GeoJsonLayer } from "deck.gl";
import chroma from "chroma-js";
import { connect } from "react-redux";
const MAPBOX_TOKEN =
"pk.mykey";
const mapStyle = "mapbox://mymapstyle";
function getEnergyFillColor(regime) {
const ENERGY_COLORS = {
naturalGaz: "#FF8500",
coal: "#99979A",
nuclear: "#E0399E",
hydroelectric: "#0082CB",
Wind: "#00B53B",
solar: "#DBCA00",
oil: "#FF0009",
other: "#FFEFD3"
};
let color;
switch (regime) {
case "Hydraulique":
color = ENERGY_COLORS.hydroelectric;
break;
case "Thermique":
color = ENERGY_COLORS.nuclear;
break;
default:
color = ENERGY_COLORS.other;
break;
}
color = chroma(color)
.alpha(0.667)
.rgba();
color[3] *= 255;
return color;
}
function _onClick(info) {
if (info.object) {
// eslint-disable-next-line
alert(
`${info.object.properties.NOM} (${info.object.properties.PROVINCE}) - ${
info.object.properties.PUISSANCE
}MW`
);
}
}
function Map({ mainProjects }) {
const { viewport, setViewport, onLoad } = useContext(MapContext);
const [airports, setAireports] = useState();
const [electricalEnergy, setElectricalEnergy] = useState();
const [hospitals, setHospitals] = useState();
const [roads, setRoads] = useState();
useEffect(() => {
if (mainProjects.length) {
setHospitals(mainProjects[0].hospitals);
setAireports(mainProjects[1].aeroports);
setElectricalEnergy(mainProjects[2].electricite);
setRoads(mainProjects[2].routes);
}
}, [airports, electricalEnergy, hospitals, roads]);
const layers = [
//ENERGIE ELECTRIQUE
new GeoJsonLayer({
id: "energy",
data: electricalEnergy,
// Styles
filled: true,
pointRadiusMinPixels: 20,
opacity: 1,
pointRadiusScale: 2000,
getRadius: energyItem => energyItem.properties.puissance * 3.14,
getFillColor: energyItem =>
getEnergyFillColor(energyItem.properties.regime),
// Interactive props
pickable: true,
autoHighlight: true,
onClick: _onClick
}),
//AEROPORTS
new GeoJsonLayer({
id: "airports",
data: airports,
// Styles
filled: true,
pointRadiusMinPixels: 20,
opacity: 1,
pointRadiusScale: 2000,
getRadius: energyItem => energyItem.properties.PUISSANCE * 3.14,
getFillColor: energyItem =>
getEnergyFillColor(energyItem.properties.REGIME),
// Interactive props
pickable: true,
autoHighlight: true,
onClick: _onClick
}),
//HOSPITALS
new GeoJsonLayer({
id: "hospitals",
data: hospitals,
// Styles
filled: true,
pointRadiusMinPixels: 20,
opacity: 1,
pointRadiusScale: 2000,
getRadius: energyItem => energyItem.properties.PUISSANCE * 3.14,
getFillColor: energyItem =>
getEnergyFillColor(energyItem.properties.REGIME),
// Interactive props
pickable: true,
autoHighlight: true,
onClick: _onClick
}),
//ROUTES
new GeoJsonLayer({
id: "roads",
data: roads,
pickable: true,
stroked: false,
filled: true,
extruded: true,
lineWidthScale: 20,
lineWidthMinPixels: 2,
getFillColor: [160, 160, 180, 200],
getLineColor: d => [255, 160, 20, 200],
// getLineColor: d => colorToRGBArray(d.properties.color),
getRadius: 100,
getLineWidth: 1,
getElevation: 30,
onHover: ({ object, x, y }) => {
// const tooltip = object.properties.name || object.properties.station;
/* Update tooltip
http://deck.gl/#/documentation/developer-guide/adding-interactivity?section=example-display-a-tooltip-for-hovered-object
*/
},
onClick: _onClick
})
];
return (
<>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.css"
rel="stylesheet"
/>
<DeckGL
initialViewState={viewport}
viewState={viewport}
controller={true}
layers={layers}
onLoad={onLoad}
onViewportChange={nextViewport => setViewport(nextViewport)}
>
<StaticMap mapboxApiAccessToken={MAPBOX_TOKEN} mapStyle={mapStyle} />
</DeckGL>
</>
);
}
const mapStateToProps = ({
selectedLinks: { sectorSelected, provinceSelected, subSectorSelected },
mainProjects
}) => {
if (sectorSelected || provinceSelected || subSectorSelected) {
return {
mainProjects
};
} else {
return {
mainProjects: []
};
}
};
export default connect(mapStateToProps)(Map);
In the above code,i try to update my local state values by is setters,but useEffect doesn't seem to work.And it looks like it's only called once,at when the component renders for the first time.How can i solve this problem?
Thank you!!

Your useEffect has a set of dependencies that donˋt match those, which are actually used.
You are setting your local state with elements of mainProjects, so useEffect will only do something when mainProjects changes.
You donˋt seem to be doing anything with your useState-Variables, so you donˋt change state, so react doesnˋt rerender.
Update: it is really important to check, that the dependency-array (2nd argument to useEffect) and the used variables inside the function (1st argument) correspond, else bad things will happen ;-)
There is an eslint-rule for that: https://www.npmjs.com/package/eslint-plugin-react-hooks

Related

ChartJS 3 Doesn't Show Data Until A Legend Is Clicked

I get some data from back-end to show. When I inspect element with React Developer Tools, I can see that data is there but not shown in production. ChartJS version is 3.8, not react-chartjs
I was having the same problem in development, too, but solved it by setting a unique key with key={Math.random()}. In development build, it works just fine. Problem occurs in production. I deploy my app on Firebase.
I wait for data before rendering:
{isAnyFetching ? "Loading..." : <BarChart01 data={chartData} width={595} height={248} key={Math.random()} />}
I tried giving an array of zeroes until data is loaded to be sure chartData changed to trigger re-render by changing the state of the chart component. I also tried giving an extraKey prop and change it with useEffect to re-render again.
The whole chart component is:
function BarChart01({
data,
width,
height
}) {
const canvas = useRef(null);
const legend = useRef(null);
useEffect(() => {
const ctx = canvas.current;
// eslint-disable-next-line no-unused-vars
const chart = new Chart(ctx, {
type: 'bar',
data: data,
options: {
layout: {
padding: {
top: 12,
bottom: 16,
left: 20,
right: 20,
},
},
scales: {
y: {
grid: {
drawBorder: false,
},
ticks: {
maxTicksLimit: 6,
callback: (value) => formatValue(value),
},
},
x: {
type: 'time',
time: {
parser: 'MM-DD-YYYY',
unit: 'month',
displayFormats: {
month: 'MMM YY',
},
},
grid: {
display: false,
drawBorder: false,
},
},
},
plugins: {
legend: {
display: true,
},
tooltip: {
callbacks: {
title: () => false, // Disable tooltip title
label: (context) => formatValue(context.parsed.y),
},
},
},
interaction: {
intersect: false,
mode: 'nearest',
},
animation: {
duration: 500,
},
maintainAspectRatio: false,
resizeDelay: 200,
},
plugins: [{
id: 'htmlLegend',
afterUpdate(c, args, options) {
const ul = legend.current;
if (!ul) return;
// Remove old legend items
while (ul.firstChild) {
ul.firstChild.remove();
}
// Reuse the built-in legendItems generator
const items = c.options.plugins.legend.labels.generateLabels(c);
items.forEach((item) => {
const li = document.createElement('li');
li.style.marginRight = tailwindConfig().theme.margin[4];
// Button element
const button = document.createElement('button');
button.style.display = 'inline-flex';
button.style.alignItems = 'center';
button.style.opacity = item.hidden ? '.3' : '';
button.onclick = () => {
c.setDatasetVisibility(item.datasetIndex, !c.isDatasetVisible(item.datasetIndex));
c.update();
};
// Color box
const box = document.createElement('span');
box.style.display = 'block';
box.style.width = tailwindConfig().theme.width[3];
box.style.height = tailwindConfig().theme.height[3];
box.style.borderRadius = tailwindConfig().theme.borderRadius.full;
box.style.marginRight = tailwindConfig().theme.margin[2];
box.style.borderWidth = '3px';
box.style.borderColor = item.fillStyle;
box.style.pointerEvents = 'none';
// Label
const labelContainer = document.createElement('span');
labelContainer.style.display = 'flex';
labelContainer.style.alignItems = 'center';
const value = document.createElement('span');
value.style.color = tailwindConfig().theme.colors.slate[800];
value.style.fontSize = tailwindConfig().theme.fontSize['3xl'][0];
value.style.lineHeight = tailwindConfig().theme.fontSize['3xl'][1].lineHeight;
value.style.fontWeight = tailwindConfig().theme.fontWeight.bold;
value.style.marginRight = tailwindConfig().theme.margin[2];
value.style.pointerEvents = 'none';
const label = document.createElement('span');
label.style.color = tailwindConfig().theme.colors.slate[500];
label.style.fontSize = tailwindConfig().theme.fontSize.sm[0];
label.style.lineHeight = tailwindConfig().theme.fontSize.sm[1].lineHeight;
const theValue = c.data.datasets[item.datasetIndex].data.reduce((a, b) => a + b, 0);
const valueText = document.createTextNode(formatValue(theValue));
const labelText = document.createTextNode(item.text);
value.appendChild(valueText);
label.appendChild(labelText);
li.appendChild(button);
button.appendChild(box);
button.appendChild(labelContainer);
labelContainer.appendChild(value);
labelContainer.appendChild(label);
ul.appendChild(li);
});
},
}],
});
chart.update();
return () => chart.destroy();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return (
<>
<div className="px-5 py-3">
<ul ref={legend} className="flex flex-wrap"></ul>
</div>
<div className="grow">
<canvas ref={canvas} width={width} height={height}></canvas>
</div>
</>
);
}
According to the code, it seems like
button.onclick = () => {
c.setDatasetVisibility(item.datasetIndex,!c.isDatasetVisible(item.datasetIndex));
c.update();
};
part in forEach loop is responsible of this update operation when I click on a label. So it somehow doesn't call update function in production as it should as useEffect listens to data prop.

Issue rendering react charts

I am dynamically changing the values that are displayed on the areachart. But for some reason the chart is only displayed if I change one of the dynamic variable with a hard coded number in the array. For example
const data = [
["Year", "Free Cash Flow", "Revenue"],
[this.props.date1, this.props.cashFlow1, this.props.revenue1],
[this.props.date2, this.props.cashFlow2, this.props.revenue2],
[this.props.date3, this.props.cashFlow3, this.props.revenue3],
[this.props.date4, this.props.cashFlow4, this.props.revenue4],
[this.props.date5, this.props.cashFlow5, this.props.revenue5],
];
that is how I structured my data array, but it doesn't renders and give the following error All series on a given axis must be of the same data type. However, if I replace this.props.revenue1 with let say 100 the area chart renders
const data = [
["Year", "Free Cash Flow", "Revenue"],
[this.props.date1, this.props.cashFlow1, 100],
[this.props.date2, this.props.cashFlow2, this.props.revenue2],
[this.props.date3, this.props.cashFlow3, this.props.revenue3],
[this.props.date4, this.props.cashFlow4, this.props.revenue4],
[this.props.date5, this.props.cashFlow5, this.props.revenue5],
];
I have looked at other examples and I can't seem to find a mistake I could've made.
import React, {Component} from "react";
import { Chart } from "react-google-charts";
class AreaChart extends Component {
render () {
const chartEvents = [
{
callback: ({ chartWrapper, google }) => {
const chart = chartWrapper.getChart();
chart.container.addEventListener("click", (ev) => console.log(ev))
},
eventName: "ready"
}
];
const rev1 = this.props.revenue1;
const FCF1 = this.props.cashFlow1;
const data = [
["Year", "Free Cash Flow", "Revenue"],
[this.props.date1, this.props.cashFlow1, this.props.revenue1],
[this.props.date2, this.props.cashFlow2, this.props.revenue2],
[this.props.date3, this.props.cashFlow3, this.props.revenue3],
[this.props.date4, this.props.cashFlow4, this.props.revenue4],
[this.props.date5, this.props.cashFlow5, this.props.revenue5],
];
const options = {
isStacked: true,
height: 300,
legend: { position: "top", maxLines: 3 },
vAxis: { minValue: 0 },
};
return (
<Chart
chartType="AreaChart"
width="75%"
height="400px"
data={data}
options={options}
chartEvents={chartEvents}
/>
);
}
}
export default AreaChart;

Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array

I'm trying to convert a Class-based component to a Functional component. I get the above-mentioned error if I use the same code that was under componentDidMount in useEffect hook.
// Class based component
class Container extends Component {
state = {
elements: [],
counter: 1,
bgColor: "#ffffff",
botTextColor: "#000000",
botBGColor: "#aaaaaa",
userTextColor: "#000000",
userBgColor: "#aaaaaa",
};
componentDidMount = async () => {
this.setState({
bgColor: this.props.chat.currentChat.bgColor,
botTextColor: this.props.chat.currentChat.botTextColor,
botBGColor: this.props.chat.currentChat.botBGColor,
userTextColor: this.props.chat.currentChat.userTextColor,
userBgColor: this.props.chat.currentChat.userBgColor,
});
this.setState({
elements:
this.props.chat.currentChat.elements &&
this.props.chat.currentChat.elements.length > 0
? elements
: [
{
id: "0",
data: {
label: (
<WelcomeNode
id={"0"}
images={this.props.chat.media.map((e) => e.file)}
updateChoices={(choices) =>
this.updateChoices("0", choices)
}
updateMessage={(message) =>
this.updateMessage("0", message)
}
updateImage={(e) => this.updateImage(e, "0")}
addEdge={this.addEdgeCustom}
deleteEdgeChoice={(index) =>
this.deleteEdgeChoice("0", index)
}
isChoiceConnected={(index) =>
this.isChoiceConnected("0", index)
}
></WelcomeNode>
),
message: "",
choices: [],
type: "welcome",
id: "0",
},
className: "node-elements",
position: { x: 100, y: 100 },
},
],
counter: elements.length > 0 ? elements.length : 1,
});
}
}
The Following is the functional component where the error occurs
// Functional component
const initialState = {.....}
const Container = () => {
const [state, setState] = useState(initialState);
const { auth, chat } = useSelector((state) => ({ ...state }));
const dispatch = useDispatch();
const history = useHistory();
useEffect(() => {
setState({
...state,
bgColor: chat.currentChat.bgColor,
botTextColor: chat.currentChat.botTextColor,
botBGColor: chat.currentChat.botBGColor,
userTextColor: chat.currentChat.userTextColor,
userBgColor: chat.currentChat.userBgColor,
});
setState({
...state,
elements:
chat.currentChat.elements && chat.currentChat.elements.length > 0
? elements
: [
{
id: "0",
data: {
label: (
<WelcomeNode
id={"0"}
images={chat.media.map((e) => e.file)}
updateChoices={(choices) => updateChoices("0", choices)}
updateMessage={(message) => updateMessage("0", message)}
updateImage={(e) => updateImage(e, "0")}
addEdge={(e) => addEdgeCustom(e)}
deleteEdgeChoice={(index) =>
deleteEdgeChoice("0", index)
}
isChoiceConnected={(index) =>
isChoiceConnected("0", index)
}
></WelcomeNode>
),
message: "",
choices: [],
type: "welcome",
id: "0",
},
className: "node-elements",
position: { x: 100, y: 100 },
},
],
counter: elements.length > 0 ? elements.length : 1,
});
}, []);
}
The following error is thrown and the browser crashes Uncaught (in promise) Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
sorry but your code is too complicated to read please reorganize it to make it more readable and understandable. please try charging useSelector line to this line:
const { auth, chat } = useSelector((state) => state);
this is causing multi render because useSelector detect state is recreating(using spread operator) so it would rerender the component.
plus in useEffect when you are setting the state use setState callback, this will not override your previous state update :
setState(prev=>({...prev,newState}))
useEffect usually requires a dependency array. What you use inside of the useEffect hook should go into that array for example we have a function that sets the id. The useEffect dependency will want the id in the array. Thus only update/run this useEffect hook if the id changes.
useEffect(() => {
setId(id)
}, [id])
If you only want to run the useEffect once on first render you can leave the array blank like this:
useEffect(()=>{
//http fetch request or something
}, [])

Change colour of clicked item in deck.gl

I'm trying to achieve the same behaviour that we have with the autoHighlight prop on mobile where clicking on an item 'selects' it and changes its colour. I managed to make it work with the updateTriggers prop of deck.gl but I feel like I'm missing something because the update takes 3 seconds and the layer isn't responsive in this time.
Here's what a have so far:
const App = () => {
const [selectedItem, setSelectedItem] = useState(null);
const layers = [
new H3ClusterLayer({
id: 'h3-cluster-layer',
data,
pickable: true,
stroked: true,
filled: true,
extruded: false,
getHexagons: d => d.hexIds,
getFillColor: hexagon => {
if (selectedItem && hexagon.hexIds[0] === selectedItem.hexIds[0]) return [25,116,210]
if (hexagon.mean === undefined) return [122, 122, 122];
return [255, (1 - hexagon.mean / 500) * 255, 0];
},
updateTriggers: {
getFillColor: [selectedItem]
},
getLineColor: [255, 255, 255],
lineWidthMinPixels: 1,
opacity: 0.03,
autoHighlight: true,
highlightColor: [142,223,255],
}),
];
return (
<DeckGL
initialViewState={initialViewState}
controller={true}
layers={layers}
onClick={(info, event) => {
// event bubble cancelation doesn't work between react components and deck.gl
if (event.target !== document.getElementById('view-default-view')) return;
setSelectedItem(info.object)
}}
>
<StaticMap mapboxApiAccessToken={MAPBOX_ACCESS_TOKEN} />
{selectedItem && <Details selectedItem={selectedItem} />}
</DeckGL>
);
}
For anyone coming across this: rerendering of the layer works perfectly if you only pass the id (rather than the whole object) to updateTriggers. In my case I just needed to pass the following to updateTriggers:
updateTriggers: {
getFillColor: [selectedItem ? selectedItem.hexIds[0]: null]
},

Line 16: React Hook "useState" is called conditionally

enter image description here
const Credits = ({ cast, baseUrl }) => {
if (!cast) {
return;
}
const [totalShow, setTotalShow] = useState(null);
const sliderElement = useRef();
// Set amount of items to show on slider based on the width of the element
const changeTotalShow = () => {
let totalItems = Math.round(sliderElement.current.offsetWidth / 70);
if (totalItems > cast.length) {
totalItems = cast.length;
}
setTotalShow(totalItems);
};
const items = cast.map(person => ());
useEffect(() => {
changeTotalShow();
window.addEventListener("resize", changeTotalShow);
return () => window.removeEventListener("resize", changeTotalShow);
}, []);
const settings = {
dots: false,
infinite: true,
autoplay: true,
autoplaySpeed: 3000,
swipeToSlide: true,
speed: 500,
slidesToShow: totalShow,
slidesToScroll: 1,
nextArrow: ,
prevArrow: ,
};
return { items };
};
First you should look at the rule of hooks, so you know what you can and can't do with hooks. You will notice that you can't have hooks called conditionally which means with every render you should have the same number of hooks. You dont as it can be that if !credits the render will not call anything. I presume if you will move
if (!cast) {
return;
}
below any hooks the warning/error will go away.

Resources