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

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]);

Related

change background image from parent with child data

im doing a weather app, and i need to change the background-image when the weather change.
in the parent i get my coords, and pass the coords to my weatherApp.jsx
(parent)
import { useEffect, useState } from 'react'
import './App.css'
import WeatherApp from './components/WeatherApp'
function App() {
const [coords, setCoords] = useState()
useEffect(() => {
const success = (pos) => {
const location = {
lat: pos.coords.latitude,
lon: pos.coords.longitude
}
setCoords(location)
}
navigator.geolocation.getCurrentPosition(success)
}, [])
console.log(coords);
return (
<div className="App">
<WeatherApp lon={coords?.lon} lat={coords?.lat}/>
</div>
)
}
export default App
in the child i use axios to get my data form openweathermap, but i need to change the background image from App.jsx
import axios from 'axios'
import React, { useEffect, useState } from 'react'
const WeatherApp = ({lon, lat}) => {
const [weather, setWeather] = useState()
const [temperature, setTemperature] = useState()
const [isCeslsius, setIsCeslsius] = useState(true)
useEffect(() => {
if (lat) {
const APIKey = "***"
const URL = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lon}&appid=${APIKey}`
axios.get(URL)
.then(res => {
setWeather(res.data)
const temp = {
celsius: {
celsius_temp: `${Math.round(res.data.main.temp - 273.15)}°C`,
celsius_min: `${Math.round(res.data.main.temp_min - 273.15)}°C`,
celsius_max: `${Math.round(res.data.main.temp - 273.15)}°C`
},
farenheit: {
farenheit_temp: `${Math.round((res.data.main.temp - 273.15) * 9/5 + 32)}°F`,
farenheit_min: `${Math.round((res.data.main.temp_min - 273.15) * 9/5 + 32)}°F`,
farenheit_max: `${Math.round((res.data.main.temp_max - 273.15) * 9/5 + 32)}°F`
}
}
setTemperature(temp)
// trying to change the background image depending on the weather description react
if (weather.weather[0].description === "clear sky") {
let bgImg = {
backgrounImage: "url(../public/clear-sky.jpg)"
}
}
})
.catch(err => console.log(err))
}
}, [lat, lon])
console.log(weather);
const changeTemperature = () => setIsCeslsius(!isCeslsius)
maybe i could just use one component but i know there is a way to pass information from child to parent i just dont know how to
You need to lift the state up. You can do something like this.
App.jsx
const [bgImg, setBgImg] = useState('');
<div className="App" style={{backgroundImage: bgImg}}>
Pass setBgImage to child
<WeatherApp setBgImg={setBgImg} lon={coords?.lon} lat={coords?.lat}/>
In WeatherApp.jsx
let bgImg = "url(../public/clear-sky.jpg)";
props.setBgImg(bgImg);
This should set the background image of parent.
You can use useState hook and initialize it with the default value in parent component. Then pass the setter function to the WeatherApp component as p props.
In parent component
const [bgImgUrl, setBgImgUrl] = useState('your default URL here');
Pass the props to the WeatherApp component
<WeatherApp lon={coords?.lon} lat={coords?.lat} setBgImgUrl={setBgImgUrl} />
Then in the weather App component, you use to write your function
....
setTemperature(temp)
if (weather.weather[0].description === "clear sky") {
let bgImg = {
backgrounImage: "url(../public/clear-sky.jpg)"
}
// update the state
props.setBgImgUrl(bgImg)
}
....

React get value from key:value array

Beginner question. I know this is a simple question but I haven't been able to get this to work. I'm passing an object which holds an array of k:v pairs to a component. Eventually this props will contain multiple k:v pairs, but for now I'm just passing the one.
[{goal: 20000}]
In the component I'm trying to grab the value, 20000, so I can display it on screen. I can't seem to get just the number. If I look at props.goal I get the entire k:v.
[{goal: 20000}]
If I try props[0].goal I get 'TypeError: undefined is not an object (evaluating 'props[0].goal')'
What am I missing? Thanks for any help.
Update:
Here is the entire code for the component in question.
import { React, useState } from "react";
import Form from "react-bootstrap/Form";
import { Row, Col, Button } from "react-bootstrap";
import "./../css/Goal.css";
const Goal = (props) => {
// const [goal, setGoal] = useState("");
const [record, setRecord] = useState("");
const monthlyGoal = 2;
console.log("props[0]");
console.log(props[0]); //undefined
console.log("props");
console.log({ props }); //See below
props: Object
goal: Object
goals: [{goal: 20000}] (1)
const handleInput = (event) => {
console.log(event);
event.preventDefault();
setRecord(event.target.value);
console.log(record);
};
const defaultOptions = {
significantDigits: 2,
thousandsSeparator: ",",
decimalSeparator: ".",
symbol: "$",
};
const formattedMonthlyGoal = (value, options) => {
if (typeof value !== "number") value = 0.0;
options = { ...defaultOptions, ...options };
value = value.toFixed(options.significantDigits);
const [currency, decimal] = value.split(".");
return `${options.symbol} ${currency.replace(
/\B(?=(\d{3})+(?!\d))/g,
options.thousandsSeparator
)}${options.decimalSeparator}${decimal}`;
};
return (
<Form>
<Row className="align-items-center flex">
<Col sm={3} className="goal sm={3}">
<Form.Control
id="inlineFormInputGoal"
placeholder="Goal"
// onChange={(e) => setGoal(e.target.value)}
/>
<Button type="submit" className="submit btn-3" onSubmit={handleInput}>
Submit
</Button>
</Col>
<Col>
<h1 className="text-box">
Goal: {formattedMonthlyGoal(monthlyGoal)}
</h1>
</Col>
</Row>
</Form>
);
};
export default Goal;
Update 2:Here is the parent component:
import React, { useEffect, useState } from "react";
import Goal from "./Goal";
import axios from "axios";
const Dashboard = () => {
const [dashboardinfo, setdashboardinfo] = useState([]);
useEffect(() => {
async function fetchData() {
try {
const data = (await axios.get("/api/goals/getgoals")).data;
setdashboardinfo(data);
} catch (error) {
console.log(error);
}
}
fetchData();
}, []);
return (
<React.Fragment>
<Goal dashboardinfo={dashboardinfo} />
</React.Fragment>
);
};
export default Dashboard;
If you get an object like the following from console logging destructured props:
{
dashboardinfo: {goals: [{goal: 20000}]}
}
You need to use props.dashboardinfo.goals[0].goal to get the value.
Your props contains the object "dashboardinfo" so you need to do
props.dashboardinfo.goals[0].goal
or a better way is to destructure your props object like this
const Goal = ({dashboardinfo: { goals }}) => {
...
goals[0].goal
...
}
I believe I've resolved my issue. It wasn't so much a problem with accessing the key:value as I thought, because when the page was initialized I was able to grab the value and display it fine. However, when I refreshed the page I lost all of the props data and that resulted in an error. I tracked it down to the useState didn't seem to be updating the value before I was trying to read it. So I added a useEffect in the child component.
const Goal = (props) => {
const [goal, setgoal] = useState([]);
useEffect(() => {
setgoal(props.goal);
console.log("the goal", goal);
}, [props.goal, goal]);
...
This seems to have worked as I'm getting the information I want and not getting any errors when I refresh. This may not be the ideal way to go about this but it is working.

React using fetch returns undefined until save

new to react so I am not quite sure what I am doing wrong here... I am trying to call data from an API, then use this data to populate a charts.js based component. When I cmd + s, the API data is called in the console, but if I refresh I get 'Undefined'.
I know I am missing some key understanding about the useEffect hook here, but i just cant figure it out? All I want is to be able to access the array data in my component, so I can push the required values to an array... ive commented out my attempt at the for loop too..
Any advice would be greatly appreciated! My not so functional code below:
import React, {useState, useEffect} from 'react'
import {Pie} from 'react-chartjs-2'
const Piegraph = () => {
const [chartData, setChartData] = useState();
const [apiValue, setApiValue] = useState();
useEffect(async() => {
const response = await fetch('https://api.spacexdata.com/v4/launches/past');
const data = await response.json();
const item = data.results;
setApiValue(item);
chart();
},[]);
const chart = () => {
console.log(apiValue);
const success = [];
const failure = [];
// for(var i = 0; i < apiValue.length; i++){
// if(apiValue[i].success === true){
// success.push("success");
// } else if (apiValue[i].success === false){
// failure.push("failure");
// }
// }
var chartSuccess = success.length;
var chartFail = failure.length;
setChartData({
labels: ['Success', 'Fail'],
datasets: [
{
label: 'Space X Launch Statistics',
data: [chartSuccess, chartFail],
backgroundColor: ['rgba(75,192,192,0.6)'],
borderWidth: 4
}
]
})
}
return (
<div className="chart_item" >
<Pie data={chartData} />
</div>
);
}
export default Piegraph;
There are a couple issues that need sorting out here. First, you can't pass an async function directly to the useEffect hook. Instead, define the async function inside the hook's callback and call it immediately.
Next, chartData is entirely derived from the apiCall, so you can make that derived rather than being its own state variable.
import React, { useState, useEffect } from "react";
import { Pie } from "react-chartjs-2";
const Piegraph = () => {
const [apiValue, setApiValue] = useState([]);
useEffect(() => {
async function loadData() {
const response = await fetch(
"https://api.spacexdata.com/v4/launches/past"
);
const data = await response.json();
const item = data.results;
setApiValue(item);
}
loadData();
}, []);
const success = apiValue.filter((v) => v.success);
const failure = apiValue.filter((v) => !v.success);
const chartSuccess = success.length;
const chartFail = failure.length;
const chartData = {
labels: ["Success", "Fail"],
datasets: [
{
label: "Space X Launch Statistics",
data: [chartSuccess, chartFail],
backgroundColor: ["rgba(75,192,192,0.6)"],
borderWidth: 4,
},
],
};
return (
<div className="chart_item">
<Pie data={chartData} />
</div>
);
};
export default Piegraph;
pull your chart algorithm outside or send item in. Like this
useEffect(async() => {
...
// everything is good here
chart(item)
})
you might wonder why I need to send it in. Because inside useEffect, your apiValue isn't updated to the new value yet.
And if you put the console.log outside of chart().
console.log(apiData)
const chart = () => {
}
you'll get the value to be latest :) amazing ?
A quick explanation is that, the Piegraph is called whenever a state is updated. But this update happens a bit late in the next cycle. So the value won't be latest within useEffect.

unable to show remaining time in wavesurfer.js

import React,{ useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from 'react-redux'
import { format, formWaveSurferOptions } from '../../utils/generic'
import { receiveCurrentTrack, receiveTrackRef , togglePlay} from '../../redux/action/currentTrack'
import './_playlist.scss'
import WaveSurfer from "wavesurfer.js";
import {Link} from 'react-router-dom';
const PlayList = (props) => {
const dispatch = useDispatch()
const { track, queue, currentTrack, index } = props
const waveformRef = useRef(null);
const waveRef = useSelector((state)=>state.currentTrack.waveRef)
const wavesurfer = useRef(null);
const currentTime = useRef(null)
const [duration, setDuration] = useState()
useEffect(() => {
const options = formWaveSurferOptions(waveformRef.current);
wavesurfer.current = WaveSurfer.create(options);
wavesurfer.current.load(track.url);
wavesurfer.current.on("ready", ()=> {
if(wavesurfer.current){
wavesurfer.current.setVolume(0)
setDuration(format(wavesurfer.current.getDuration()))
}
});
return () => wavesurfer.current.destroy();
}, [track.url]);
useEffect(()=>{
if(currentTrack){
dispatch(receiveTrackRef(wavesurfer))
}
return ()=> waveRef?.current.stop()
},[currentTrack.track?.url])
const handleClick = () => {
dispatch(receiveTrackRef(wavesurfer))
if(currentTrack){
dispatch(togglePlay())
}
else {
waveRef?.current.stop()
dispatch(receiveCurrentTrack(track, queue));
}
};
return (
<div
className={ currentTrack ? "playlist-item selected playlist" : "playlist-item playlist"}
onClick={handleClick}
>
<div># {index} :</div>
<Link to='/track'>{track.title}</Link>
<div>{track.artist}</div>
<div><span>0.00</span> / {duration && duration}</div>
<div className="wave-form" id="waveform" onChange={()=>console.log('hhh')} ref={waveformRef} />
<div>{duration && duration}</div>
</div>
);
};
export default PlayList;
React-Wavesurfer was unmaintained, so i have moved on to the wavesurfer.js but the issue arrises the now how i can detect the audio current time and show to the page e.g. 0:01 / 3.02, i have use wavesurfer.on('audioprocess') but i can not detect it, so if any one knows the solution please help, thankyou :)
From the Official Doc :
getCurrentTime() – Returns current progress in seconds.
There's also a audioprocess event as you mentioned that fires continuously as the audio plays.
So by combining these together, we have this:
let current;
wavesurfer.on('audioprocess', () => {
current = wavesurfer.getCurrentTime();
});
Note that your wavesurfer player is assigned to the name wavesurfer.current. so the code should be wavesurfer.current.on('audioprocess', () => {});

The React useState() hooks stores undefined always even the data that is to be stored logs correctly using console.log();

Here is the where I am having the problem,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
Here when I am trying to log the 'parsedId' it logs the data correctly
ioNng23DkIM
And after using the setVideoId() function when I try to log the value it returns undefined
undefined
Here is a snap shot of the log output.
Home.js code:
import React, { useRef, useState } from "react";
import { Link } from "react-router-dom";
import getYouTubeID from 'get-youtube-id';
function Home(props) {
const [videoLink, setVideoLink] = useState();
const [isBool, setBool] = useState(false);
const [videoId, setVideoId] = useState();
const urlRef = useRef();
const handleChange = (event) => {
setVideoLink(event.target.value);
if (urlRef.current.value === '') {
alert('Please enter a URL');
setBool(true);
} else {
setBool(false);
}
}
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId);
console.log(videoId);
}
return (
<section className="homeLayout">
<div className="logo-display">
<img className="logo-img" alt="logo" src="./logo.png" />
<h1>WatchIt</h1>
</div>
<div className="searchlayer">
<form>
<input ref={urlRef} id="videoLink" placeholder="Enter the youtube video URL:" onBlur={handleChange} required />
<Link style={{ pointerEvents: isBool ? 'none' : 'initial' }} to={`/play?=${videoId}`} onClick={handleCLick}>Play</Link>
</form>
</div>
</section>
);
}
export default Home;
You can use useEffect to solve your problem.
Use effect will listen to you state change n then you can perform logic in there.
The problem you're facing is because setState will set the value eventually, not immediately (Usually this means the update will be visible when the component is rendered again). If you want to do something after the value is set, you need to use useEffect.
Splitting your handleClick we get,
const handleCLick = () => {
const parsedId = getYouTubeID(videoLink);
console.log(parsedId);
setVideoId(parsedId); // Queue the change for `videoId`
}
useEffect(() => {
console.log(videoId);
}, [videoId]); // Call this function when the value of `videoId` changes

Resources