AMCharts - map disappearing after data is set on MapPolygonSeries (React) - reactjs

I hope someone can help with this one.
I'm using AMCharts packages as dependencies in the React app. Everything works till I set data on MapPolygonSeries (note: it works if all is set inside the same useLayoutEffect(), but that may work for basic setup but for further development is not an option).
Here is a code example.
import { useLayoutEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import * as am5 from '#amcharts/amcharts5';
import * as am5map from '#amcharts/amcharts5/map';
import am5themes_Animated from '#amcharts/amcharts5/themes/Animated';
import am5geodata_worldLow from '#amcharts/amcharts5-geodata/worldLow';
const RootDiv = styled.div`
widht: 100%;
height: 500px;
`;
const AmChartsMapView = () => {
const worldSeriesRef = useRef(null);
const [worldData, setWorldData] = useState([]);
// for this example assume that data is correctly structured (e.g. [{id: 'countryId', value: 'some number'}] )
fetch('http://some-location-data.com/data.json')
.then((response) => response.json())
.then((data) => setWorldData(data));
useLayoutEffect(() => {
const root = am5.Root.new('rootdiv');
root.setThemes([am5themes_Animated.new(root)]);
const chart = am5map.MapChart.new(root, {
panX: 'translateX',
panY: 'translateY',
projection: am5map.geoMercator(),
});
root.container.children.push(chart);
const worldSeries = am5map.MapPolygonSeries.new(root, {
geoJSON: am5geodata_worldLow,
exclude: ['AQ'],
valueField: 'value',
calculateAggregates: true,
});
worldSeries.mapPolygons.template.setAll({
tooltipText: '{name}: {value}',
interactive: true,
fill: am5.color(0xaaaaaa),
templateField: 'polygonSettings',
});
chart.series.push(worldSeries);
worldSeriesRef.current = worldSeries;
return () => {
if (root) {
root.dispose();
rootRef.current = null;
}
}
},[]);
useLayoutEffect(() => {
if (!worldSeriesRef.current || !worldData.length) {
return;
}
//when this is added map disappear
worldSeriesRef.current.data.setAll(worldData);
}, [worldSeriesRef.current, worldData.length]);
return <RootDiv id="rootdiv" />;
}

Related

when building a project the customizable hook does not work correctly

I am developing a project, I created a custom hook, using localhost, the project works correctly, it lists the information, however with the project built and sent to the cloud, the data does not return to me, as if the re-render did not work to launch the data from a .map on screen
Three files are being used to get this information:
one containing a JSON with the list of requests,
another containing the custom hook,
and another containing the function call with a .map
// listRequest.js
export const listUrlGestaoQualidade = [
{
url: 'cadastro-liberacao-termofusaos',
name: 'Pré - Qualificação de solda por Termorusão',
qtd: 0,
aprovado: 0,
reprovado: 0,
aproveitamento: 0
},
];
// hooks
import { useCallback, useContext } from "react";
import { GlobalState } from "../store";
import { listUrlGestaoQualidade } from "../json/listRequests";
import api from "../services/api";
const tokenUser = localStorage.getItem('#app-token');
const licenca = localStorage.getItem('#app-licenca');
const projeto = localStorage.getItem('#projeto');
export const useGetQualidade = () => {
const { setRegistersQualidade } = useContext(GlobalState);
const executeGetQualidade = useCallback(async () => {
const params = `?filters[licenca][$eq]=${licenca}&filters[numero_projeto][$eq]=${projeto}`;
listUrlGestaoQualidade.forEach(async (request, index) => {
await api.get(request.url + params, { headers: { Authorization: `Bearer ${tokenUser}` } })
.then(res => {
if (request.name === 'Pré - Qualificação de solda por Termorusão') {
const statusAprovado = [];
const statusReprovado = [];
if (res.data.data.length > 0) {
res.data.data.forEach(status => {
if (status.attributes.status_deslocamento === 'Aprovado' && status.attributes.status_cisalhamento === 'Aprovado') {
statusAprovado.push('Aprovado')
} else {
statusReprovado.push('Reprovado')
}
});
listUrlGestaoQualidade[index].qtd = res.data.data.length;
listUrlGestaoQualidade[index].aprovado = statusAprovado.length;
listUrlGestaoQualidade[index].reprovado = statusReprovado.length;
listUrlGestaoQualidade[index].aproveitamento = (statusAprovado.length / res.data.data.length * 100).toFixed(2)
}
}
}).catch(err => {
});
});
console.log('listUrlGestaoQualidade', listUrlGestaoQualidade)
setRegistersQualidade(listUrlGestaoQualidade)
}, [setRegistersQualidade]);
return { executeGetQualidade };
}
// hook call containing .map
import { useContext, useEffect } from 'react';
import { GlobalState } from '../../../store';
import { useGetQualidade } from '../../../hooks/useGetQualidade';
import { Container, Table, AreaRows, SessionTable, TitleSession, Title, AreaTable } from "./styles";
export default function Acompanhamento() {
const { registersQualidade } = useContext(GlobalState);
const { executeGetQualidade } = useGetQualidade();
useEffect(() => {
executeGetQualidade();
}, [executeGetQualidade]);
return (
<Container>
<Table>
<AreaRows>
<td colSpan={9}>
<SessionTable>
<TitleSession>
<Title>Gestão de Qualidade e Ensaios de Campo</Title>
</TitleSession>
<AreaTable>
<table>
<tr>
<th>Descrição</th>
<th>Quantidade</th>
<th>Aprovado</th>
<th>Reprovado</th>
<th>Aproveitamento %</th>
</tr>
{registersQualidade.map(rows => (
<tr>
<td>{rows.name}</td>
<td>{rows.qtd}</td>
<td>{rows.aprovado}</td>
<td>{rows.reprovado}</td>
<td>{rows.aproveitamento}%</td>
</tr>
))}
</table>
</AreaTable>
</SessionTable>
</td>
</AreaRows>
</Table>
</Container>
);
}
In localhost, it works correctly listing the information, but the build in the cloud does not list, guys if anyone can help me I would be very grateful.
It seems the re-render is not working, so I tried using the global state with useContext
Thank you for your attention #skyboyer, in my case I needed to loop through each url of the array and make a request, but the foreach does not wait for each promise to be resolved, it passes directly without waiting, for that you must use Promisse.all(), or if you use axios axios.all
here is the resolution of my problem, using Prommise.all which expects all promises to be resolved
import { useState, useCallback } from 'react';
import api from '../services/api';
export const useGetLancamentos = () => {
const [acumulado, setAcumulado] = useState([]);
const executeGetAcumulados = useCallback(async () => {
const tokenUser = localStorage.getItem('#app-token');
const licenca = localStorage.getItem('#app-licenca');
const projeto = localStorage.getItem('#projeto');
const params = `?filters[licenca][$eq]=${licenca}&filters[numero_projeto][$eq]=${projeto}`;
const endpoints = [
'lancamento-geomembranas',
'lancamento-geotextils',
'lancamento-geogrelhas',
'lancamento-gcls',
'lancamento-geocelulas',
'lancamento-geocompostos',
];
Promise.all(endpoints.map((endpoint) => api.get(endpoint + params, { headers: { Authorization: `Bearer ${tokenUser}` } }))).then(allResponses => {
console.log(allResponses);
setAcumulado(allResponses);
});
}, []);
return { executeGetAcumulados }
}

React native custom hook return null in the first time using react-native-geolocation-services custom

I am trying to create a custom hook to get user's current location. I am using react-native-geolocation-services.
It returns null for the first time.
However, when I try to re-run the app. The geo data shows again.
Is this issue happening in asyn data?
Am I wrongly implemented the usestate so that the data didn't show in the first time?
Map component
import {useCurrentLocation} from '../queries/getCurrentLocation';
const Map = () => {
const {coordinate, watchError} = useCurrentLocation();
console.log('data',coordinate)
return <View style={styles.container}><MapView /></View>;
};
Custome Hook
import React, {useRef, useState, useEffect } from 'react';
import Geolocation, {watchPosition} from 'react-native-geolocation-service';
import useLocationPermission from '../hooks/useLocationPermission';
export const useCurrentLocation = () => {
const [coordinate, setCoordinate] = useState(null);
const [watchError, setWatchError] = useState(null);
const watchId = useRef(null);
const {hasPermission, hasPermissionError} = useLocationPermission();
const startWatch = () => {
if (!hasPermission) return;
watchId.current = Geolocation.watchPosition(
position => {
const latitude = position.coords.latitude;
const longitude = position.coords.longitude;
const speed = position.coords.speed;
setCoordinate({latitude, longitude, speed});
},
error => {
setWatchError(error);
},
{
accuracy: {
android: 'high',
//TODO config to ios
//ios: 'best',
},
enableHighAccuracy: true,
distanceFilter: 0,
interval: 20000,
fastestInterval: 2000,
},
);
};
const stopWatch = () => {
if (watchId.current == null) return;
Geolocation.clearWatch(watchId.current);
watchId.current = null;
};
useEffect(() => {
if (hasPermission) {
getCurrentCoordinate(coordinate);
}
startWatch();
return () => {
stopWatch();
};
}, [coordinate]);
return {coordinate, watchError};
};
const getCurrentCoordinate = coordinate => {
Geolocation.getCurrentPosition(position => {
coordinate = position;
});
return coordinate;
};

Component that loads an external script working in React but not Next.js

so I have a component that loads an external script to display some Instagram posts in a grid on my page and it works fine in a create-react-app application, but once I put the same component into an (unmodified) create-next-app, it still inserts the correct DOM elements but doesn't run the script to resize them to the window size anymore.
Do you maybe have some ideas what differences between plain React and Next.js could be causing this?
This is the component:
import PropTypes from 'prop-types'
import React, { useEffect } from 'react'
const loadFlowbox = () => new Promise(resolve => {
(function(d, id) {
if (!window.flowbox) { var f = function () { f.q.push(arguments); }; f.q = []; window.flowbox = f; }
if (d.getElementById(id)) {return resolve();}
var s = d.createElement('script'), fjs = d.scripts[d.scripts.length - 1]; s.id = id; s.async = true;
s.src = 'https://connect.getflowbox.com/flowbox.js';
fjs.parentNode.insertBefore(s, fjs);
resolve()
})(document, 'flowbox-js-embed');
})
const containerName = 'js-flowbox-flow'
const locale = 'en-GB'
const Flowbox = ({ flowKey: key }) => {
const container = `${containerName}-${key}`
useEffect(() => {
loadFlowbox().then(() => {
window.flowbox('init', { container: `#${container}`, key, locale })
})
}, [container, key])
return <div id={container} />
}
Flowbox.propTypes = {
flowKey: PropTypes.string.isRequired,
}
export default Flowbox
Edit: So I managed to solve it by putting crossOrigin: "anonymous" into my next.config.js
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
crossOrigin: "anonymous",
};
module.exports = nextConfig;
I'm not exactly sure why it works now but it seems to be some kind of CORS issue. Also for this specific script (Flowbox-flow) it seems to break when the containing element has display: flex set on it.

React-Leaflet: Cannot create a Polyline DYNAMICALLY from React Context

UPDATE!
As Seth Luke asked, why a ref instead of a state, so I did that, and now the lines get drawn! but one step behind. Check out these lines:
useEffect(()=>{
if (drawing) {
setZonePolygon((prev)=>[...prev, [clickLocation.lat, clickLocation.lng]]);
setContextData((prevContext)=>({...prevContext, lines: zonePolygon}));
addZoneMarker();
}
}, [clickLocation]);
"lines" in context is getting updated one step behind the local state "zonePolygon"... how do I correct this? Even if I switch the calls, it's the same, the Context gets updated with a delay...
ORIGINAL POST:
I'm connected to a context in my main map component which contains a . I'm changing the context from another component expecting my map container component to update and re-render the polyline, but it is not happening. What am I doing wrong here? I'm really tired of reading and trying all sort of stuff for over 15 hours no rest now. Could anybody help please? I'd really appreciate it.
My goal is to let the user click different points in the map and have those joined with a line, so that then I can save that as an area or "zone".
This is not being called, I wonder why! I'm using react dev tools to debug and the context does indeed gets the changes, but it's not triggering in the component... so weird.
useEffect(()=>{
console.log('Lines updated in Map component via Context.', lines);
}, [lines]); // This is not being called, I wonder why!!! ****
This is the code I have:
import React, {useState, useEffect, useContext, useRef} from 'react';
import {MapContainer, Marker, Polyline, Polygon, useMapEvent} from 'react-leaflet';
import 'leaflet-rotatedmarker';
import {MapContext} from '../../context/MapProvider';
import Layers from './Layers';
import Ships from '../Ships';
const Map = () => {
const [map, setMap] = useState(null);
const {contextData, setContextData} = useContext(MapContext);
const {clickLocation, drawing, lines} = contextData;
const [shipData, setShipData] = useState();
useEffect(()=>{
console.log('Lines updated in Map component via Context.', lines);
}, [lines]); // This is not being called, I wonder why!!! ****
useEffect(()=>{
if (!map) return;
setContextData({...contextData, mapRef: map});
}, [map]);
useEffect(() => {
setShipData(contextData.vessels);
}, [contextData.vessels]);
function MapEvents() {
const map = useMapEvent('click', (e) => {
setContextData({...contextData, clickLocation: e.latlng});
});
return null;
}
// const ZONE = [
// [-41.95208616893812, -73.52483926124243],
// [-42.246913395396184, -73.17047425039003],
// [-42.19905906325171, -72.68013196793146],
// [-41.936746304733255, -72.81473573174362],
// [-41.8118450173935, -73.22404105435608],
// ]
return (
<MapContainer
center={[-42, -73]}
zoom={10}
style={{height: '100%', width: '100%'}}
whenCreated={setMap}>
<MapEvents />
<Layers />
<Ships data={shipData} />
{
(drawing & lines.length > 1) ? <Polyline positions={lines} /> : null
}
</MapContainer>
)
}
export default Map;
And this is where I'm modifying the context at:
import React, {useState, useEffect, useRef, useContext} from 'react';
import L from 'leaflet';
import styles from '../../styles.module.scss';
import ZoneItem from './ZoneItem';
import { MapContext } from './../../../../context/MapProvider';
const ZonesBar = () => {
const {contextData, setContextData} = useContext(MapContext);
const {mapRef, drawing, lines, clickLocation} = contextData;
const [zones, setZones] = useState([]);
const [zoneMarkers, setZoneMarkers] = useState([]);
let zonePolygon = useRef([]);
useEffect(()=>{
if (drawing) {
setContextData((contextData)=>({...contextData, lines: []}));
zonePolygon.current = [];
} else if (!drawing) {
if (zonePolygon.current.length > 2) {
setContextData((prevContext)=>({...prevContext, zones: [...prevContext.zones, contextData.lines]}));
setZones((prevZones)=>([...prevZones, zonePolygon.current]));
clearMarkers();
}
}
}, [drawing]);
useEffect(()=>{
if (drawing) {
zonePolygon.current.push([clickLocation.lat, clickLocation.lng]);
setContextData((prevContext)=>({...prevContext, lines: zonePolygon.current}));
addZoneMarker();
}
}, [clickLocation]);
function toggleDrawing() {
setContextData((prevContext)=>({...prevContext, drawing: !prevContext.drawing}))
}
function addZoneMarker() {
const newMarker = L.marker([clickLocation.lat, clickLocation.lng])
.addTo(mapRef);
setZoneMarkers((prevMarkers)=>([...prevMarkers, newMarker]));
}
function clearMarkers() {
zoneMarkers.forEach(m => mapRef.removeLayer(m));
}
return (
<div className={styles.zones}>
<button
className={`${styles.btn_add} ${drawing ? styles.btn_drawing : ''}`}
onClick={toggleDrawing}
>
{drawing ? 'Agregar zona' : 'Definir zona'}
</button>
<span style={{fontSize: '0.7rem', fontStyle: 'italic', marginLeft: '0.5rem',}}>
{drawing ? 'Dar clicks en el mapa para definir la zona, luego presionar el botón otra vez.' : ''}
</span>
<div className={styles.list}>
{
zones.length > 0 ?
zones.map(zone => <ZoneItem data={zone} />)
:
'Lista vacía.'
}
</div>
</div>
)
}
export default ZonesBar;
I've changed things up so much now since 9 am today, that I don't know anything else anymore. There's obviously a way of doing this, and I do need some help. If you could take your time to go through this issue that'd be life saving for me.
This is what is looks like, see when I render it with a hard-coded array the polyline comes up.
This is my Context:
import React, {useState, useEffect, createContext, useContext} from 'react'
import io from 'socket.io-client'
import axios from 'axios';
export const MapContext = createContext();
const socket = io("http://localhost:3001");
const MapProvider = ({children}) => {
const [contextData, setContextData] = useState({
mapRef: null,
clickLocation: [],
markers: [],
zones: [],
drawing: false,
lines: [],
vessels: []
});
// Bring vessels info from API and store in Context.
useEffect(()=>{
axios.get('http://localhost:3001/vessel/search/all')
.then(res => {
setContextData((prevContext)=>({...prevContext, vessels: res.data}));
})
.then(()=>{
socket.on('vessels', data => {
setContextData((prevContext)=>({...prevContext, vessels: data}));
})
})
.catch(err => console.log(err.message));
}, []);
return (
<MapContext.Provider value={{contextData, setContextData}}>
{children}
</MapContext.Provider>
)
}
export default MapProvider;
I can't see anything out of the ordinary. But try moving MapEvents outside the Map component. Something like
function MapEvents() {
const {contextData, setContextData} = useContext(MapContext);
const map = useMapEvent('click', (e) => {
setContextData({...contextData, clickLocation: e.latlng});
});
return null;
}
const Map = () => {
const [map, setMap] = useState(null);
const {contextData, setContextData} = useContext(MapContext);
const {clickLocation, drawing, lines} = contextData;
const [shipData, setShipData] = useState();
const linesForPolyline = useRef();
useEffect(()=>{
console.log('Lines updated in Map component via Context.', lines);
}, [lines]);
useEffect(()=>{
if (!map) return;
setContextData({...contextData, mapRef: map});
}, [map]);
useEffect(() => {
setShipData(contextData.vessels);
}, [contextData.vessels]);
// const ZONE = [
// [-41.95208616893812, -73.52483926124243],
// [-42.246913395396184, -73.17047425039003],
// [-42.19905906325171, -72.68013196793146],
// [-41.936746304733255, -72.81473573174362],
// [-41.8118450173935, -73.22404105435608],
// ]
return (
<MapContainer
center={[-42, -73]}
zoom={10}
style={{height: '100%', width: '100%'}}
whenCreated={setMap}>
<MapEvents />
<Layers />
<Ships data={shipData} />
{
(drawing & lines.length > 1) ? <Polyline positions={lines} /> : null
}
</MapContainer>
)
}
export default Map;
It looks like what Seth Lutske suggested in the comments to the original question plus some adjustments did the trick. I wish he could post it as an answer so that I accept it as the solution.
Basically the solution was to use a state hook:
const [zonePolygon, setZonePolygon] = useState([]);
Instead of a useRef:
const zonePolygon = useRef();
Then to have the local state and the global Context update in order I split them in different useEffects. This is the working code, but I believe it needs a refactor:
useEffect(()=>{
if (drawing) {
setZonePolygon((prev)=>[...prev, [clickLocation.lat, clickLocation.lng]]);
addZoneMarker();
}
}, [clickLocation]);
useEffect(()=>{
setContextData((prevContext)=>({...prevContext, lines: zonePolygon}));
}, [zoneMarkers]);

React-admin Datagrid: expand all rows by default

I have a react-admin (3.14.1) List using a Datagrid, where each row is expandable.
Does anyone know how to expand all the rows by default?
Or expand a row programmatically?
I've checked the Datagrid code in node_modules/ra-ui-materialui/lib/list/datagrid/Datagrid.js, no obvious props...
Datagrid.propTypes = {
basePath: prop_types_1.default.string,
body: prop_types_1.default.element,
children: prop_types_1.default.node.isRequired,
classes: prop_types_1.default.object,
className: prop_types_1.default.string,
currentSort: prop_types_1.default.shape({
field: prop_types_1.default.string,
order: prop_types_1.default.string,
}),
data: prop_types_1.default.any,
// #ts-ignore
expand: prop_types_1.default.oneOfType([prop_types_1.default.element, prop_types_1.default.elementType]),
hasBulkActions: prop_types_1.default.bool,
hover: prop_types_1.default.bool,
ids: prop_types_1.default.arrayOf(prop_types_1.default.any),
loading: prop_types_1.default.bool,
onSelect: prop_types_1.default.func,
onToggleItem: prop_types_1.default.func,
resource: prop_types_1.default.string,
rowClick: prop_types_1.default.oneOfType([prop_types_1.default.string, prop_types_1.default.func]),
rowStyle: prop_types_1.default.func,
selectedIds: prop_types_1.default.arrayOf(prop_types_1.default.any),
setSort: prop_types_1.default.func,
total: prop_types_1.default.number,
version: prop_types_1.default.number,
isRowSelectable: prop_types_1.default.func,
isRowExpandable: prop_types_1.default.func,
};
I have a solution that does something similar, not using jquery. It's a custom hook that makes the first id of a resource expand, which in my case is the first item in the List.
// useExpandFirst.ts
import * as React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Identifier, ReduxState, toggleListItemExpand } from 'react-admin';
type AboutExpansion = { noneExpanded: boolean; firstId: Identifier };
const useExpandFirst = (props) => {
const { resource } = props;
const once = React.useRef(false);
const dispatch = useDispatch();
const { noneExpanded, firstId } = useSelector<ReduxState, AboutExpansion>((state) => {
const list = state?.admin?.resources?.[resource]?.list;
return {
noneExpanded: list?.expanded?.length === 0,
firstId: list?.ids[0],
};
});
React.useEffect(() => {
if (noneExpanded && firstId && !once.current) {
once.current = true;
const action = toggleListItemExpand(resource, firstId);
dispatch(action);
}
}, [noneExpanded, firstId, dispatch, resource]);
};
Instead of using the hook in the component that actually renders the List, I'm using it in some other (not so) random component, for example the app's Layout. That causes way less rerenders of the component that renders the List.
// MyLayout.tsx
const MyLayout: React.FC<LayoutProps> = (props) => {
// expand the first company record as soon as it becomes available
useExpandFirst({ resource: 'companies' });
return (
<Layout
{...props}
appBar={MyAppBar}
sidebar={MySidebar}
menu={MyMenu}
// notification={MyNotification}
/>
);
};
It's not perfect, but it does the job. With just a few modifications, you can alter it to expand all id's. That would mean that you have to dispatch the action for each id (in the useEffect hook function).
I found this question and used this answer to solve the same problem.
export const useExpandDefaultForAll = (resource) => {
const [ids, expanded] = useSelector(
(state) => ([state.admin.resources[resource].list.ids, state.admin.resources[resource].list.expanded])
);
const dispatch = useDispatch();
useEffect(() => {
for (let i = 0; i < ids.length; i++) {
if (!expanded.includes(ids[i])){
dispatch(toggleListItemExpand(resource, ids[i]));
}
}
}, [ids]);
};
And i also call it in my List component:
const OrderList = (props) => {
useExpandDefaultForAll(props.resource);
...
I will be glad if it is useful to someone. If you know how to do it better then please fix it.
Hacked it with jquery, dear dear.
import $ from 'jquery'
import React, {Fragment} from 'react';
import {List, Datagrid, TextField, useRecordContext} from 'react-admin';
export class MyList extends React.Component {
gridref = React.createRef()
ensureRowsExpanded(ref) {
// Must wait a tick for the expand buttons to be completely built
setTimeout(() => {
if (!ref || !ref.current) return;
const buttonSelector = "tr > td:first-child > div[aria-expanded=false]"
const buttons = $(buttonSelector, ref.current)
buttons.click()
}, 1)
}
/**
* This runs every time something changes significantly in the list,
* e.g. search, filter, pagination changes.
* Surely there's a better way to do it, i don't know!
*/
Aside = () => {
this.ensureRowsExpanded(this.gridref)
return null;
}
render = () => {
return <List {...this.props} aside={<this.Aside/>} >
<Datagrid
expand={<MyExpandedRow />}
isRowExpandable={row => true}
ref={this.gridref}
>
<TitleField source="title" />
</Datagrid>
</List>
}
}
const MyExpandedRow = () => {
const record = useRecordContext();
if (!record) return "";
return <div>Hello from {record.id}</div>;
}
Relies on particular table structure and aria-expanded attribute, so not great. Works though.

Resources