Line 16: React Hook "useState" is called conditionally - reactjs

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.

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.

React useCallback function runs in infinite loop

I am trying to use leaflet-geoman library in a React project. I need to create a custom toolbar button that enables and disables global drag mode.
When toolbar button is clicked, map.pm.enableGlobalDragMode(); function enables global mode. When toolbar button is clicked again, map.pm.disableGlobalDragMode(); causes useCallback function afterClick running in infinite loop.
codesandbox.io
useDraw.js
import React from "react";
const useDraw = (mapRef) => {
const afterClick = React.useCallback(() => {
console.log("afterclick");
const map = mapRef.current.leafletElement;
let editDragEnabled = false;
if (!editDragEnabled) {
console.log("enable");
map.pm.enableGlobalDragMode();
editDragEnabled = true;
} else {
console.log("disable");
map.pm.disableGlobalDragMode();
editDragEnabled = false;
}
}, [mapRef]);
React.useEffect(() => {
const map = mapRef.current.leafletElement;
var actions = ["finishMode"];
map.pm.addControls({
drawRectangle: false,
drawMarker: false,
drawPolyline: false,
drawPolygon: false,
drawCircle: false,
drawCircleMarker: false,
removalMode: false,
editMode: false,
cutPolygon: false,
dragMode: false
});
map.pm.Toolbar.createCustomControl({
name: "DragEdit",
block: "custom",
title: "Edit and Drag Layers",
onClick: () => afterClick(),
actions: actions,
toggle: true
});
}, [mapRef, afterClick]);
};
export default useDraw;
The problem is, that when enableGlobalDragMode ( or disable) the Control of the original drag button is activated and this disables your custom button (because all other buttons are disabled, so that only one mode can be active).
I suggest to use the code from the enableGlobalDragMode function instead of calling it, which cause a change on the controls:
const afterClick = React.useCallback(() => {
console.log("afterclick");
const map = mapRef.current.leafletElement;
const layers = L.PM.Utils.findLayers(map);
let dragMode = map.pm._customModeEnabled || false;
if(!dragMode){
console.log("enable");
layers.forEach((layer)=>{
layer.pm.enableLayerDrag();
})
}else{
console.log("disable");
layers.forEach((layer)=>{
layer.pm.disableLayerDrag();
})
}
map.pm._customModeEnabled = !dragMode;
}, [mapRef]);

React mui-datatable pagination starts at 0, instead of 1

So, I have a MUI-Datatable, which I'm trying to paginate server side, these are the datatable options,
const [netflixData, setNetflixData] = useState({});
const [page, setPage] = useState(1);
const countPerPage = 10;
const getNetflixData = () => {
axios.get(`/netflix/ranks/?page=${page}`, config).then(res => {
setNetflixData(res.data);
}).catch(err => {
setNetflixData({});
});
};
const options = {
filter: true,
filterType: 'multiselect',
serverSide: true,
count: netflixData.count,
rowsPerPage: countPerPage,
rowsPerPageOptions: [],
onTableChange: (action, tableState) => {
if (action === 'changePage') {
setPage(tableState.page);
} else {
console.log('action not handled.');
}
},
};
useEffect(() =>{
getNetflixData()
}, [page]
);
<MUIDataTable
title={"Netflix Rankings"}
data={netflixData.results}
columns={columns}
options={options}
/>
Basically, on page load tableState.page should be 1, but, nothing happens, so when I click 'next page', it changes to 1, 3rd page, tableState.page is 2, so if I go back twice, it'll be 0, which doesn't exist.
I tried adding the option, page: 1, but that just defaults me to the second page of the table. Any ideas on how to set tableState.page = 1 on page/table load?
Try using setPage(tableState.page + 1). Your page number on the state will be one-based and on the datatable it will be zero-based. I don't know if you can make the datatable paging one-based.

How to convert react asynchronous tests synchronously?

Below is the test for ag-grid. Documentaion can be found at https://www.ag-grid.com/javascript-grid-testing-react/
Few of my tests are failing in CI when tests are asynchronous as test1. Is there any solution to make it consistent? I tried test2 approach make it synchronous but that is also failing. Is there any better way to run tests with consistency?
describe('ag grid test 1', () => {
let agGridReact;
let component;
const defaultProps = {
//.....
}
beforeEach((done) => {
component = mount(<CustomAGGridComp {...defaultProps} />);
agGridReact = component.find(AgGridReact).instance();
// don't start our tests until the grid is ready
ensureGridApiHasBeenSet(component).then(() => done(), () => fail("Grid API not set within expected time limits"));
});
it('stateful component returns a valid component instance', async () => {
expect(agGridReact.api).toBeTruthy();
//..use the Grid API...
var event1 = {
type: 'cellClicked', rowIndex: 0, column: { field: "isgfg", colId: "isgfg", headerName: "Property 2" },
event: {
ctrlKey: false,
shiftKey: false
}
}
await agGridReact.api.dispatchEvent(event1)
//some expect statements
})
});
describe('ag grid test 2', () => {
let agGridReact;
let component;
const defaultProps = {
//.....
}
beforeEach((done) => {
component = mount(<CustomAGGridComp {...defaultProps} />);
agGridReact = component.find(AgGridReact).instance();
// don't start our tests until the grid is ready
ensureGridApiHasBeenSet(component).then(() => done(), () => fail("Grid API not set within expected time limits"));
});
it('stateful component returns a valid component instance', () => {
expect(agGridReact.api).toBeTruthy();
//..use the Grid API...
var event1 = {
type: 'cellClicked', rowIndex: 0, column: { field: "isgfg", colId: "isgfg", headerName: "Property 2" },
event: {
ctrlKey: false,
shiftKey: false
}
}
agGridReact.api.dispatchEvent(event1);
setTimeout(() => {
//some expect statements
}, 500);
})
});

React useEffect doesn't change data displayed on map

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

Resources