I saw quite a few posts about a delay between setting the state for a component, however, I'm running into this issue in a custom hook that I built. Basically, the classNames that I'm returning are being applied, just with a delay after the component first renders. I've tried using callback functions and useEffect with no luck. Does anyone have any ideas about why there is a small delay?
import * as React from 'react';
import classNames from 'classnames';
import debounce from 'lodash/debounce';
const useScrollStyling = ref => {
const [isScrolledToBottom, setIsScrolledToBottom] = React.useState(false);
const [isScrolledToTop, setIsScrolledToTop] = React.useState(true);
const [isOverflowScrollingEnabled, setIsOverflowScrollingEnabled] = React.useState(false);
const { current } = ref;
React.useEffect(() => {
if (current) {
const { clientHeight, scrollHeight } = current;
setIsOverflowScrollingEnabled(scrollHeight > clientHeight);
}
}, [current]);
const handleScroll = ({ target }) => {
const { scrollHeight, scrollTop, clientHeight } = target;
const isScrolledBottom = scrollHeight - Math.ceil(scrollTop) === clientHeight;
const isScrolledTop = scrollTop === 0;
setIsScrolledToBottom(isScrolledBottom);
setIsScrolledToTop(isScrolledTop);
};
return {
handleScroll: React.useMemo(() => debounce(handleScroll, 100), []),
scrollShadowClasses: classNames({
'is-scrolled-top': isOverflowScrollingEnabled && isScrolledToTop,
'is-scrolled-bottom': isScrolledToBottom,
'is-scrolled': !isScrolledToTop && !isScrolledToBottom,
}),
};
};
export default useScrollStyling;
i made login hook called "useMakeQueryString". which is responsble for making queryString from Object.
import { useEffect, useState } from "react";
export default function useMakeQueryString(obj) {
const [queryString, setQueryString] = useState("");
const makeQueryString=(obj)=> {
let queryString1 = "";
for (let key in obj) {
//if (key instanceof Object) queryString1 += makeQueryString(obj);
queryString1 += key + "=" + obj[key] + "&";
}
setQueryString(queryString1.slice(0, queryString1.length - 1));
}
useEffect(()=>{
makeQueryString(obj)
},[obj])
return { queryString, makeQueryString };
}
then i imported that hook to my Google Component. on click of Component it calls the performAuth function and that function set the option state. and useEffect on option change is called. inside useEffect which is being called on option change i try to change queryString State. but the problem is useEffect on queryString change is being Called Twice
import useMakeQueryString from "../Login/LoginHook";
import { useEffect,useState } from "react";
export default function Google() {
const g_url = "https://accounts.google.com/o/oauth2/v2/auth";
const {queryString,makeQueryString} = useMakeQueryString({});
let [option,setOption] = useState({})
useEffect(() => {
console.log("length"+Object.keys(option).length)
if(Object.keys(option).length!=0) {
makeQueryString(option); // setQueryString(query);
}
}, [option])
useEffect(()=>{
if(queryString)
window.location = `${g_url}?${queryString}`;
},[queryString])
const performAuth = () => {
console.log("perform cliked")
const option1 = {
client_id: "432801522480-h02v02ivvti9emkd019fvreoisgj3umu.apps.googleusercontent.com",
redirect_uri: "http://localhost:3000/glogin",
response_type: "token",
scope: [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
].join(" "),
}
setOption(option1);
}
return (
<>
<button className="google-btn social-btn" onClick={() => performAuth()}>SignUp With Google</button>
</>
)
}
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]);
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', () => {});
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