How to use setTimeout within a .map() in reactJS - javascript

I have an array of players which I map through. The user is constantly adding new players to this array. I map through the array and return a set of elements. I'd like each player from the list to render one at a time with a 500ms interval in between. Here's the code I have so far:
export const ShowDraftedPlayers = props => {
const {
draftedPlayers,
getPlayerProfile,
teams,
draftPos,
myTeam } = props;
let playersDraftedList = draftedPlayers.map((player, index) => {
return (
<div key={index} className='drafted'>
<p style={style}>TEAM {player.teamDraftedBy} </p>
<b className='player-lastName'> {player.displayName} </b>
{player.position}
</p>
</div>
)
})
return (
<div className='draftedPlayers-container'>
{playersDraftedList}
</div>
)
}

You can use an effect to create an interval which updates a counter every 500ms; your render logic can then render a growing slice of the array each time.
export const ShowDraftedPlayers = props => {
const {
draftedPlayers,
getPlayerProfile,
teams,
draftPos,
myTeam
} = props;
const [count, setCount] = useState(0);
useEffect(() => {
let counter = count;
const interval = setInterval(() => {
if (counter >= draftedPlayers.length) {
clearInterval(interval);
} else {
setCount(count => count + 1);
counter++; // local variable that this closure will see
}
}, 500);
return () => clearInterval(interval);
}, [draftedPlayers]);
let playersDraftedList = draftedPlayers.slice(0, count).map((player, index) => {
return (
<div key={index} className='drafted'>
<p style={style}>TEAM {player.teamDraftedBy} </p>
<b className='player-lastName'> {player.displayName} </b>
{player.position}
</p>
</div>
)
})
return (
<div className='draftedPlayers-container'>
{playersDraftedList}
</div>
)
}

You don't. setTimeout is asynchronous, as so is JavaScript and React. Components are rendered as soon as possible so that JavaScript can jump to do something else. The best way is to create a hook that reacts (pun intended) to some change in value, in this case props.draftedPlayers, setTimeout for 500ms, then add that new player to an internal players state and let the component renders based on it.
Here is how the component will reflect a change in props.draftedPlayers with a delay of 500ms:
export const ShowDraftedPlayers = props => {
const {
draftedPlayers,
getPlayerProfile,
teams,
draftPos,
myTeam
} = props;
const [players, setPlayers] = useState([]);
// Set internal `players` state to the updated `draftedPlayers`
// only when `draftedPlayers` changes.
useEffect(() => {
const delay = setTimeout(() => {
setPlayers(draftedPlayers);
}, 500);
return () => clearTimeout(delay);
}, [draftedPlayers]);
let playersDraftedList = players.map((player, index) => {
return (
<div key={index} className='drafted'>
<p style={style}>TEAM {player.teamDraftedBy} </p>
<b className='player-lastName'> {player.displayName} </b>
{player.position}
</p>
</div>
);
};
};
Whenever draftedPlayers prop is changed (players added or removed) from the outer context, the component will update its players array after a delay of 500ms.

Related

Why does my toast notification not re-render in React?

I am trying to create my own "vanilla-React" toast notification and I did manage to make it work however I cannot wrap my head around why one of the solutions that I tried is still not working.
So here we go, onFormSubmit() I want to run the code to get the notification. I excluded a bunch of the code to enhance readability:
const [notifications, setNotifications] = useState<string[]>([]);
const onFormSubmit = (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();
const newNotifications = notifications;
newNotifications.push("success");
console.log(newNotifications);
setNotifications(newNotifications);
};
return (
<>
{notifications.map((state, index) => {
console.log(index);
return (
<ToastNotification state={state} instance={index} key={index} />
);
})}
</>
</section>
);
Inside the Toast I have the following:
const ToastNotification = ({
state,
instance,
}:
{
state: string;
instance: number;
}) => {
const [showComponent, setShowComponent] = useState<boolean>(true);
const [notificationState, setNotificationState] = useState(
notificationStates.empty
);
console.log("here");
const doNotShow = () => {
setShowComponent(false);
};
useEffect(() => {
const keys = Object.keys(notificationStates);
const index = keys.findIndex((key) => state === key);
if (index !== -1) {
const prop = keys[index] as "danger" | "success";
setNotificationState(notificationStates[prop]);
}
console.log(state);
}, [state, instance]);
return (
<div className={`notification ${!showComponent && "display-none"}`}>
<div
className={`notification-content ${notificationState.notificationClass}`}
>
<p className="notification-content_text"> {notificationState.text} </p>
<div className="notification-content_close">
<CloseIcon color={notificationState.closeColor} onClick={doNotShow} />
</div>
</div>
</div>
);
};
Now for the specific question - I cannot understand why onFormSubmit() I just get a log with the array of strings and nothing happens - it does not even run once - the props get updated with every instance and that should trigger a render, the notifications are held into a state and even more so, should update.
What is wrong with my code?

Why is prior useEffect() still running for a significant amount of time even after the component is rendered?

I'm implementing stopwatch in ReactJs this is how my code looks as of now:
const App: React.FC = () => {
const [seconds, setSeconds] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const secondsToTimerFormat = (seconds: number): string => {
console.log(seconds)
return (seconds-seconds%60)/60+":"+seconds%60
}
const manipulateTimer = (toPauseTimer: boolean) => {
setIsPaused(toPauseTimer);
}
useEffect(() => {
if(!isPaused){
setTimeout(() => {
setSeconds(seconds + 1)
}, 1000)
}
}, [seconds, isPaused])
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button onClick={() => {manipulateTimer(true)}}>Pause</button>
<button onClick={() => {manipulateTimer(false)}}>Resume</button>
<button onClick={() => {
setSeconds(0);
}}>Reset</button>
</div>
</div>
);
}
I'm expecting this to work normally. But the "Reset" button is not working as expected.
If I click on "Reset" after 13 seconds, this is the console.log() output.
If I add a new variable inside useEffect(), say something like let execute: boolean = true and then set it to false in useEffect() clean up, everything is working as expected.
So, I know the fix, but I want to know the reason behind the current behaviour. I understand that when I click on reset, there is already a useEffect() running with seconds value as 13. But since its setTimeout() ends in one second and at the same time, I'm doing setSeconds(0), why would the previous useEffect() run multiple times before coming to halt?
Issues like this usually arise because the timers being used are not being cleared between renders. Also, when the next state depends on the current state, it is better to use the second form of the state setter function which takes the current state as the parameter and returns the next state. Modify the useEffect as given below to get this to work:
useEffect(() => {
let timer;
if (!isPaused) {
timer = setTimeout(() => {
setSeconds((seconds) => seconds + 1);
}, 1000);
}
return () => {
if (timer) clearTimeout(timer);
};
}, [seconds, isPaused]);
Try using setInterval and separate methods for handling the timer state:
import { useState } from "react";
export default function App() {
const [seconds, setSeconds] = useState(0);
const [intervalId, setIntervalId] = useState(0);
const secondsToTimerFormat = (seconds) => {
console.log(seconds);
return (seconds - (seconds % 60)) / 60 + ":" + (seconds % 60);
};
const handleStart = () => {
const id = setInterval(() => {
setSeconds((prev) => prev + 1);
}, 1000);
setIntervalId(id);
};
const handlePause = () => {
clearInterval(intervalId);
};
const handleReset = () => {
handlePause();
setSeconds(0);
};
return (
<div className="App">
{secondsToTimerFormat(seconds)}
<div>
<button
onClick={() => {
handlePause();
}}
>
Pause
</button>
<button
onClick={() => {
handleStart();
}}
>
Resume
</button>
<button
onClick={() => {
handleReset();
}}
>
Reset
</button>
</div>
</div>
);
}
Link to sandbox

Ref doesn't get updated when state change and ref is set to another div

When I set the opacity of element in someFunc, it doesn't set last element's opacity but one before. Ref is not updated instantly.
Elements' opacity will stay "1" in normal. This opacity change is needed just for the last added element and for short time.
How can I access the last element of that div in the same line of someFunc ?
const MyComponent = () => {
const [divisions, setDivisions] = useState([]);
const itemRef = useRef();
const someFunc = () => {
const newDivision = {
id: "someId",
someData: "someText"
}
setDivisions(prevState => [...prevState, newDivision])
itemRef.current.style.opacity = "0.7"
}
return (
<>
<div onClick={someFunc}>Button</div>
<div className="container">
{divisions.map((item, key) => {
return <div id={item.id} ref={itemRef}>item.someData</div>
})
}
</div>
</>
)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
You can easily add the ref to the last item by counting divisions (minus one as you're matching to the key which starts at zero - I added brackets ( ... ) for clarity but they're optional):
ref={key === (divisions.length - 1) && itemRef}
That is because you are using the same ref on the mapped elements. simply change your div to the below
<div id={item.id} ref={(el) => (itemRef.current[key] = el)}>item.someData</div>
and your function to this
const someFunc = () => {
const newDivision = {
id: "someId",
someData: "someText"
}
setDivisions(prevState => [...prevState, newDivision])
for (let i = 0; i < itemRef.current.length; i++) {
if(i === (itemRef.current.length - 1))
{
itemRef.current[i].style.opacity = "0.7"
}
}
}
then inform react by calling useEffect
useEffect((console.log(itemRef)) => {},[itemRef])

React hooks: get current value from child component with no constant rerender

There is counter on page. To avoid re-rendering the entire Parent component every second, the counter is placed in a separate Child component.
From time to time there is need to take current time from counter Child (in this example by clicking Button).
I found solution with execution useEffect by passing empty object as dependency.
Even though it works, I don't feel this solution is correct one.
Do you have suggestion how this code could be improved?
Parent component:
const Parent = () => {
const [getChildValue, setGetChildValue] = useState(0);
const [triggerChild, setTriggerChild] = useState(0); // set just to force triggering in Child
const fooJustToTriggerChildAction = () => {
setTriggerChild({}); // set new empty object to force useEffect in child
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
triggerChild={triggerChild}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
triggerChild,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
const valueForParent = totalTimeRef.current;
handleValueFromChild(valueForParent); // use Parent's function to pass new time
}, [triggerChild]); // Force triggering with empty object
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};
Given your set of requirements, I would do something similar, albeit with one small change.
Instead of passing an empty object (which obviously works, as {} !== {}), I would pass a boolean flag to my child, requesting to pass back the current timer value. As soon as the value is passed, I would then reset the flag to false, pass the value and wait for the next request by the parent.
Parent component:
const Parent = () => {
const [timerNeeded, setTimerNeeded] = useState(false);
const fooJustToTriggerChildAction = () => {
setTimerNeeded(true);
};
const handleValueFromChild = (timeFromChild) => {
console.log('Current time from child:', timeFromChild);
setTimerNeeded(false);
};
return (
<>
<Child
handleValueFromChild={handleValueFromChild}
timerNeeded={timerNeeded}
/>
<Button onPress={fooJustToTriggerChildAction} >
Click to take time
</Button>
</>
);
};
Child component
const Child = ({
timerNeeded,
handleValueFromChild,
}) => {
const [totalTime, setTotalTime] = useState(0);
const totalTimeRef = useRef(totalTime); // useRef to handle totalTime inside useEffect
const counter = () => {
totalTimeRef.current = totalTimeRef.current + 1;
setTotalTime(totalTimeRef.current);
setTimeout(counter, 1000);
};
useEffect(() => {
counter();
}, []); // Run time counter at first render
useEffect(() => {
if (timerNeeded) {
handleValueFromChild(totalTimeRef.current);
}
}, [timerNeeded]);
return (
<>
<div>Total time: {totalTime}</div>
</>
);
};

Countdown timer using React Hooks

So the timer works. If I hard code this.state with a specific countdown number, the timer begins counting down once the page loads. I want the clock to start counting down on a button click and have a function which changes the null of the state to a randomly generated number. I am a bit new to React. I am know that useState() only sets the initial value but if I am using a click event, how do I reset useState()? I have been trying to use setCountdown(ranNum) but it crashes my app. I am sure the answer is obvious but I am just not finding it.
If I didnt provide enough code, please let me know. I didn't want to post the whole shebang.
here is my code:
import React, { useState, useEffect } from 'react';
export const Timer = ({ranNum, timerComplete}) => {
const [ countDown, setCountdown ] = useState(ranNum)
useEffect(() => {
setTimeout(() => {
countDown - 1 < 0 ? timerComplete() : setCountdown(countDown - 1)
}, 1000)
}, [countDown, timerComplete])
return ( <p >Countdown: <span>{ countDown }</span> </p> )
}
handleClick(){
let newRanNum = Math.floor(Math.random() * 20);
this.generateStateInputs(newRanNum)
let current = this.state.currentImg;
let next = ++current % images.length;
this.setState({
currentImg: next,
ranNum: newRanNum
})
}
<Timer ranNum={this.state.ranNum} timerComplete={() => this.handleComplete()} />
<Button onClick={this.handleClick} name='Generate Inputs' />
<DisplayCount name='Word Count: ' count={this.state.ranNum} />
You should store countDown in the parent component and pass it to the child component. In the parent component, you should use a variable to trigger when to start Timer.
You can try this:
import React from "react";
export default function Timer() {
const [initialTime, setInitialTime] = React.useState(0);
const [startTimer, setStartTimer] = React.useState(false);
const handleOnClick = () => {
setInitialTime(5);
setStartTimer(true);
};
React.useEffect(() => {
if (initialTime > 0) {
setTimeout(() => {
console.log("startTime, ", initialTime);
setInitialTime(initialTime - 1);
}, 1000);
}
if (initialTime === 0 && startTimer) {
console.log("done");
setStartTimer(false);
}
}, [initialTime, startTimer]);
return (
<div>
<buttononClick={handleOnClick}>
Start
</button>
<Timer initialTime={initialTime} />
</div>
);
}
const Timer = ({ initialTime }) => {
return <div>CountDown: {initialTime}</div>;
};
useState sets the initial value just like you said, but in your case I don't think you want to store the countDown in the Timer. The reason for it is that ranNum is undefined when you start the application, and gets passed down to the Timer as undefined. When Timer mounts, useEffect will be triggered with the value undefined which is something you don't want since it will trigger the setTimeout. I believe that you can store countDown in the parent of the Timer, start the timeout when the button is clicked from the parent and send the countDown value to the Timer as a prop which would make the component way easier to understand.
Here is a simple implementation using hooks and setInterval
import React, {useState, useEffect, useRef} from 'react'
import './styles.css'
const STATUS = {
STARTED: 'Started',
STOPPED: 'Stopped',
}
export default function CountdownApp() {
const [secondsRemaining, setSecondsRemaining] = useState(getRandomNum())
const [status, setStatus] = useState(STATUS.STOPPED)
const handleStart = () => {
setStatus(STATUS.STARTED)
}
const handleStop = () => {
setStatus(STATUS.STOPPED)
}
const handleRandom = () => {
setStatus(STATUS.STOPPED)
setSecondsRemaining(getRandomNum())
}
useInterval(
() => {
if (secondsRemaining > 0) {
setSecondsRemaining(secondsRemaining - 1)
} else {
setStatus(STATUS.STOPPED)
}
},
status === STATUS.STARTED ? 1000 : null,
// passing null stops the interval
)
return (
<div className="App">
<h1>React Countdown Demo</h1>
<button onClick={handleStart} type="button">
Start
</button>
<button onClick={handleStop} type="button">
Stop
</button>
<button onClick={handleRandom} type="button">
Random
</button>
<div style={{padding: 20}}>{secondsRemaining}</div>
<div>Status: {status}</div>
</div>
)
}
function getRandomNum() {
return Math.floor(Math.random() * 20)
}
// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
const savedCallback = useRef()
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current()
}
if (delay !== null) {
let id = setInterval(tick, delay)
return () => clearInterval(id)
}
}, [delay])
}
Here is a link to a codesandbox demo: https://codesandbox.io/s/react-countdown-demo-random-c9dm8?file=/src/App.js

Categories