How can I access the updated value of the state? I am getting updated values on UI but not while accessing state value on the fly.
const OTP = () => {
const [counter, setCounter] = useState(3);
const [someState, setSomeState] = useState("something");
useEffect(() => {
startCountdownTimer();
}, []);
const countdownTimerFunc = () => {
let value = counter; // <--- initial value always
// someState <--- initial value always
console.log('==== countdown: START====', counter);
if (value && value > 0) {
setCounter(prev => prev - 1); // Updating right value
} else {
console.log('==== countdown: STOP====');
}
};
const startCountdownTimer = () => {
setSomeState("updated something");
internalID = BackgroundTimer.setInterval(() => {
countdownTimerFunc();
}, 1000);
};
const counterUI = () => {
return (
<Text>{counter}</Text>
)
}
...
return (
<View>
{counterUI()}
</View>
)
};
export default OTP;
Update: What is the right structure to access the state? Is it only with useEffect or can we handle with custom hook which will return state? The difficulty which am seeing in my structure to access the value inside seperate functions.
Please replace below code and let me know
const OTP = () => {
const [counter, setCounter] = useState(3);
const [someState, setSomeState] = useState("something");
useEffect(() => {
if (counter > 0) {
countdownTimerFunc()
}else{
startCountdownTimer();
}
}, [counter]);
const countdownTimerFunc = () => {
let value = counter; // <--- initial value always
// someState <--- initial value always
console.log('==== countdown: START====', counter);
if (value && value > 0) {
setCounter(counter - 1); // Updating right value
} else {
console.log('==== countdown: STOP====');
}
};
const startCountdownTimer = () => {
setSomeState("updated something");
setTimeout(() => {
countdownTimerFunc();
}, 1000);
};
const counterUI = () => {
return (
<Text>{counter}</Text>
)
}
...
return (
<View>
{counterUI()}
</View>
)
};
export default OTP;
Related
What I am trying to do is to update the reset the countdown after changing the status.
There are three status that i am fetching from API .. future, live and expired
If API is returning future with a timestamp, this timestamp is the start_time of the auction, but if the status is live then the timestamp is the end_time of the auction.
So in the following code I am calling api in useEffect to fetch initial data pass to the Countdown and it works, but on 1st complete in handleRenderer i am checking its status and updating the auctionStatus while useEffect is checking the updates to recall API for new timestamp .. so far its working and 2nd timestamp showed up but it is stopped ... means not counting down time for 2nd time.
import React, { useEffect } from 'react';
import { atom, useAtom } from 'jotai';
import { startTimeAtom, auctionStatusAtom } from '../../atoms';
import { toLocalDateTime } from '../../utility';
import Countdown from 'react-countdown';
import { getCurrentAuctionStatus } from '../../services/api';
async function getAuctionStatus() {
let response = await getCurrentAuctionStatus(WpaReactUi.auction_id);
return await response.payload();
}
const Counter = () => {
// component states
const [startTime, setStartTime] = useAtom(startTimeAtom);
const [auctionStatus, setAuctionStatus] = useAtom(auctionStatusAtom);
useEffect(() => {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
setStartTime(toLocalDateTime(response.end_time, WpaReactUi.time_zone));
});
}, [auctionStatus]);
//
const handleRenderer = ({ completed, formatted }) => {
if (completed) {
console.log("auction status now is:", auctionStatus);
setTimeout(() => {
if (auctionStatus === 'future') {
getAuctionStatus().then((response) => {
setAuctionStatus(response.status);
});
}
}, 2000)
}
return Object.keys(formatted).map((key) => {
return (
<div key={`${key}`} className={`countDown bordered ${key}-box`}>
<span className={`num item ${key}`}>{formatted[key]}</span>
<span>{key}</span>
</div>
);
});
};
console.log('starttime now:', startTime);
return (
startTime && (
<div className="bidAuctionCounterContainer">
<div className="bidAuctionCounterInner">
<Countdown
key={auctionStatus}
autoStart={true}
id="bidAuctioncounter"
date={startTime}
intervalDelay={0}
precision={3}
renderer={handleRenderer}
/>
</div>
</div>
)
);
};
export default Counter;
You use auctionStatus as a dependency for useEffect.
And when response.status is the same, the auctionStatus doesn't change, so your useEffect won't be called again.
For answering your comment on how to resolve the issue..
I am not sure of your logic but I'll explain by this simple example.
export function App() {
// set state to 'live' by default
const [auctionStatus, setAuctionStatus] = React.useState("live")
React.useEffect(() => {
console.log('hello')
changeState()
}, [auctionStatus])
function changeState() {
// This line won't result in calling your useEffect
// setAuctionStatus("live") // 'hello' will be printed one time only.
// You need to use a state value that won't be similar to the previous one.
setAuctionStatus("inactive") // useEffect will be called and 'hello' will be printed twice.
}
}
You can simply use a flag instead that will keep on changing from true to false like this:
const [flag, setFlag] = React.useState(true)
useEffect(() => {
// ..
}, [flag])
// And in handleRenderer
getAuctionStatus().then((response) => {
setFlag(!flag);
});
Have a look at the following useCountdown hook:
https://codepen.io/AdamMorsi/pen/eYMpxOQ
const DEFAULT_TIME_IN_SECONDS = 60;
const useCountdown = ({ initialCounter, callback }) => {
const _initialCounter = initialCounter ?? DEFAULT_TIME_IN_SECONDS,
[resume, setResume] = useState(0),
[counter, setCounter] = useState(_initialCounter),
initial = useRef(_initialCounter),
intervalRef = useRef(null),
[isPause, setIsPause] = useState(false),
isStopBtnDisabled = counter === 0,
isPauseBtnDisabled = isPause || counter === 0,
isResumeBtnDisabled = !isPause;
const stopCounter = useCallback(() => {
clearInterval(intervalRef.current);
setCounter(0);
setIsPause(false);
}, []);
const startCounter = useCallback(
(seconds = initial.current) => {
intervalRef.current = setInterval(() => {
const newCounter = seconds--;
if (newCounter >= 0) {
setCounter(newCounter);
callback && callback(newCounter);
} else {
stopCounter();
}
}, 1000);
},
[stopCounter]
);
const pauseCounter = () => {
setResume(counter);
setIsPause(true);
clearInterval(intervalRef.current);
};
const resumeCounter = () => {
setResume(0);
setIsPause(false);
};
const resetCounter = useCallback(() => {
if (intervalRef.current) {
stopCounter();
}
setCounter(initial.current);
startCounter(initial.current - 1);
}, [startCounter, stopCounter]);
useEffect(() => {
resetCounter();
}, [resetCounter]);
useEffect(() => {
return () => {
stopCounter();
};
}, [stopCounter]);
return [
counter,
resetCounter,
stopCounter,
pauseCounter,
resumeCounter,
isStopBtnDisabled,
isPauseBtnDisabled,
isResumeBtnDisabled,
];
};
I need to use a quantity for my cart, I'm learning react, but I get this warning: Warning: Received NaN for the children attribute. If this is expected, cast the value to a string.
import { createContext, useState, useContext, useEffect } from "react";
export const CartContext = createContext([]);
export const CartProvider = ({children}) => {
const [cart, setCart] = useState([]);
//const cantidadProductos = cart.length;
const [cartQuantity, setCartQuantity] = useState(0);
useEffect(() => {
const getQuantity = () => {
let cantidadTotalProductos = 0;
cart.forEach((orden)=>{
cantidadTotalProductos += Number(orden.cantidadTotalProductos);
});
setCartQuantity(cantidadTotalProductos);
}
getQuantity();
}, [cart]);
const addItem = (item, cantidad) => {
const newItem = { item, cantidad };
const itemEsta = cart.find((order) => order.item.id === item.id);
if(itemEsta){
const actualizarCarrito = cart.map((order) => {
if(order.item.id === item.id){
return {...order, cantidad: cantidad + order.cantidad};
}else{
return order;
}
});
setCart(actualizarCarrito);
}else{
setCart((prevState) => [...prevState, newItem]);
}
};
const removeItem = (id) => {
setCart((prev) => prev.filter((element) => element.item.id !== id));
};
const clearAll = () => {
setCart([]);
};
return (
<CartContext.Provider value={{ cart, addItem, removeItem, clearAll, cartQuantity }}>
{children}
</CartContext.Provider>
);
};
export const useCart = () => useContext(CartContext);
And then in the component:
import './Carrito.css';
import { useCart } from "../context/CartContext";
function Carrito () {
const { cartQuantity } = useCart();
return <div className="editarCarrito">
<p className="carritoCantidad">{ cartQuantity }</p>
</div>
}
export default Carrito;
Issue
The code is doing an arithmetic operation with a non-number type, and once this occurs the result is Nan and by definition any further arithmetic operations also always result in NaN.
useEffect(() => {
const getQuantity = () => {
let cantidadTotalProductos = 0;
cart.forEach((orden)=>{
cantidadTotalProductos += Number(orden.cantidadTotalProductos); // <-- cantidadTotalProductos is undefined
});
setCartQuantity(cantidadTotalProductos);
}
getQuantity();
}, [cart]);
const addItem = (item, cantidad) => {
const newItem = { item, cantidad }; // <-- no cantidadTotalProductos property
const itemEsta = cart.find((order) => order.item.id === item.id);
if (itemEsta) {
const actualizarCarrito = cart.map((order) => {
if (order.item.id === item.id) {
return { ...order, cantidad: cantidad + order.cantidad };
} else {
return order;
}
});
setCart(actualizarCarrito);
} else {
setCart((prevState) => [...prevState, newItem]);
}
};
Solution
I think cantidad is the quantity value you are wanting to sum over. Provide a fallback value in case Number(cantidad) isn't a number.
useEffect(() => {
const cantidadTotalProductos = cart.reduce(
(total, { cantidad }) => total + (Number(cantidad) || 0),
0
);
setCartQuantity(cantidadTotalProductos);
}, [cart]);
Suggestion
The cart item total quantity is what is considered derived state since it's easily computable from the actual cart state, and really shouldn't also be stored in state. Compute it in the render result as part of the context value.
Identify The Minimal (but complete) Representation of UI State
Let’s go through each one and figure out which one is state. Ask three
questions about each piece of data:
Is it passed in from a parent via props? If so, it probably isn’t state.
Does it remain unchanged over time? If so, it probably isn’t state.
Can you compute it based on any other state or props in your component? If so, it isn’t state.
Example:
const cartQuantity = cart.reduce(
(total, { cantidad }) => total + (Number(cantidad) || 0),
0
);
return (
<CartContext.Provider
value={{ cart, addItem, removeItem, clearAll, cartQuantity }}
>
{children}
</CartContext.Provider>
);
I have these properties declared in my app:
const [lockfileData, setLockFileData] = useState({});
const [socket, setSocket] = useState<RiotWSProtocol>(null);
const [api, setApi] = useState<LoLAPI>(null);
const [champions, setChampions] = useState<Champion[]>([]);
const [summoner, setSummoner] = useState<Summoner>(null);
const [autoAcceptQueue, setAutoAccept] = useState(true);
const [instalockEnabled, setEnableInstalock] = useState(true);
const [selectedChampion, setSelectedChampion] = useState<Champion>(null);
const [callRoleEnabled, setCallRoleEnabled] = useState(true);
const [selectedRole, setSelectedRole] = useState<Role>('Mid');
I have an event handler in my useEffect hook, and inside that it handles more events:
const onJsonApiEvent = useCallback(
(message: any) => {
//console.log(message);
if (
message.uri === '/lol-matchmaking/v1/ready-check' &&
autoAcceptQueue
) {
if (
message.data?.state === 'InProgress' &&
message.data?.playerResponse !== 'Accepted'
) {
api.acceptQueue();
}
} else if (
message.uri === '/lol-champ-select/v1/session' &&
message.eventType === 'Update'
) {
console.log('enabled?', instalockEnabled)
if (instalockEnabled) {
const myCellId = message.data.localPlayerCellId as number;
const myAction = (message.data.actions[0] as any[]).find(
(x) => x.actorCellId === myCellId
);
if (
!myAction.completed &&
myAction.isInProgress &&
myAction.type === 'pick'
) {
api.pickAndLockChampion(1, myAction.id);
}
console.log('myAction', myAction);
}
}
},
[api, autoAcceptQueue, instalockEnabled]
);
const onSocketOpen = useCallback(() => {
console.log('socket', socket);
if (socket) {
socket.subscribe('OnJsonApiEvent', onJsonApiEvent);
}
}, [onJsonApiEvent, socket]);
const onConnect = useCallback((data: LCUCredentials) => {
setLockFileData(data);
const lolApi = new LoLAPI(data);
setApi(lolApi);
lolApi.getOwnedChampions().then((champs) => {
setSelectedChampion(champs[0]);
setChampions(champs);
});
lolApi.getCurrentSummoner().then((summoner) => {
setSummoner(summoner);
});
const wss = new RiotWSProtocol(
`wss://${data.username}:${data.password}#${data.host}:${data.port}`
);
setSocket(wss);
}, []);
useEffect(() => {
if (socket) {
socket.on('open', onSocketOpen);
}
connector.on('connect', onConnect);
connector.start();
return () => {
connector.stop();
};
}, [onConnect, onSocketOpen, socket]);
The dependencies appear to be correct, so it should be using the up to date values in each handler.
However, inside the onJsonApiEvent handler, properties such as instalockEnabled are always the default value.
I am updating the value of instalockEnabled in a component on my page:
<FormControlLabel
control={
<Checkbox
checked={instalockEnabled}
name="instalockEnabled"
color="primary"
onChange={handleInstalockEnableChange}
/>
}
label="Enabled"
/>
And its handler looks like this:
const handleInstalockEnableChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
setEnableInstalock(e.target.checked);
};
How come this is happening when it is a dependency?
The janky solution I've come up with for now is to have a separate variable that is useRef and update that at the same time as updating the state, therefore it persists:
const [instalockEnabled, setEnableInstalock] = useState(true);
const instalockEnabledRef = useRef(instalockEnabled);
const handleInstalockEnableChange = (
e: React.ChangeEvent<HTMLInputElement>
) => {
setEnableInstalock(e.target.checked);
instalockEnabledRef.current = e.target.checked;
};
And then just use instalockEnabledRef.current inside of the event handlers where it needs to know the current value.
I'm trying to implement countdown timer on my own just to know hooks more. I know there are libraries out there but don't want to use it. the problem with my code is, I cannot get updated state inside "timer" function which is updated in start timer function I'm trying to implement timer that will have triggers to start, stop, & resume & can be manually trigger. by other component that is using the countdown component
import React, { useState } from 'react';
const Countdown = ({ countDownTimerOpt }) => {
const [getObj, setObj] = useState({
formatTimer: null,
countDownTimer: 0,
intervalObj: null,
});
const { formatTimer, countDownTimer, intervalObj } = getObj;
if (countDownTimerOpt > 0 && intervalObj === null) {
startTimer();
}
function startTimer() {
const x = setInterval(() => {
timer();
}, 1000);
setObj((prev) => ({
...prev,
countDownTimer: countDownTimerOpt,
intervalObj: x,
}));
}
function timer() {
var days = Math.floor(countDownTimer / 24 / 60 / 60);
var hoursLeft = Math.floor(countDownTimer - days * 86400);
var hours = Math.floor(hoursLeft / 3600);
var minutesLeft = Math.floor(hoursLeft - hours * 3600);
var minutes = Math.floor(minutesLeft / 60);
var remainingSeconds = countDownTimer % 60;
const formatTimer1 =
pad(days) +
':' +
pad(hours) +
':' +
pad(minutes) +
':' +
pad(remainingSeconds);
if (countDownTimer === 0) {
clearInterval(intervalObj);
} else {
setObj((prev) => ({
...prev,
formatTimer: formatTimer1,
countDownTimer: prev['countDownTimer'] - 1,
}));
}
}
function pad(n) {
return n < 10 ? '0' + n : n;
}
return <div>{formatTimer ? formatTimer : Math.random()}</div>;
};
export default Countdown;
import React, { useState, useEffect } from 'react';
import Timer from '../../components/countdown-timer/countdown.component';
const Training = () => {
const [getValue, setValue] = useState(0);
useEffect(() => {
const x = setTimeout(() => {
console.log('setTimeout');
setValue(10000);
}, 5000);
return () => clearInterval(x);
}, []);
return <Timer countDownTimerOpt={getValue} />;
don't want to use any set interval inside training page as the countdown component will also be used in exam page
Usually with hooks I would combine your functionality into a custom hook and use it in different places.
const useTimer = (startTime) => {
const [time, setTime] = useState(startTime)
const [intervalID, setIntervalID] = useState(null)
const hasTimerEnded = time <= 0
const isTimerRunning = intervalID != null
const update = () => {
setTime(time => time - 1)
}
const startTimer = () => {
if (!hasTimerEnded && !isTimerRunning) {
setIntervalID(setInterval(update, 1000))
}
}
const stopTimer = () => {
clearInterval(intervalID)
setIntervalID(null)
}
// clear interval when the timer ends
useEffect(() => {
if (hasTimerEnded) {
clearInterval(intervalID)
setIntervalID(null)
}
}, [hasTimerEnded])
// clear interval when component unmounts
useEffect(() => () => {
clearInterval(intervalID)
}, [])
return {
time,
startTimer,
stopTimer,
}
}
You can of course add a reset function or do other changes but use could look like this:
const Training = () => {
const { time, startTimer, stopTimer } = useTimer(20)
return <>
<div>{time}</div>
<button onClick={startTimer}>start</button>
<button onClick={stopTimer}>stop</button>
</>
}
You can create a useCountDown Hook as follow (In Typescript) :
Gist
import { useEffect, useRef, useState } from 'react';
export const useCountDown: (
total: number,
ms?: number,
) => [number, () => void, () => void, () => void] = (
total: number,
ms: number = 1000,
) => {
const [counter, setCountDown] = useState(total);
const [startCountDown, setStartCountDown] = useState(false);
// Store the created interval
const intervalId = useRef<number>();
const start: () => void = () => setStartCountDown(true);
const pause: () => void = () => setStartCountDown(false);
const reset: () => void = () => {
clearInterval(intervalId.current);
setStartCountDown(false);
setCountDown(total);
};
useEffect(() => {
intervalId.current = setInterval(() => {
startCountDown && counter > 0 && setCountDown(counter => counter - 1);
}, ms);
// Clear interval when count to zero
if (counter === 0) clearInterval(intervalId.current);
// Clear interval when unmount
return () => clearInterval(intervalId.current);
}, [startCountDown, counter, ms]);
return [counter, start, pause, reset];
};
Usage Demo: https://codesandbox.io/s/usecountdown-hook-56lqv
I'm building Carousel Component using useLayoutEffect. this useLayoutEffect hook has been set in resizeWindow.ts separately. and resizeWindow function is called in functional Component named carousel. I can't find where breaking rule is.
//resizeWindow.ts
import { useLayoutEffect, useState, RefObject } from 'react'
/***
* #function resizeWindow
* this function is custom hook for grab resizing innerWidth of element.
*
*
*/
export const resizeWindow: (ref: RefObject<HTMLElement>) => number[] = (ref) => {
const [ elementWidth, elementHeight ] = ref.current ?
[ref.current.offsetWidth, ref.current.offsetHeight ] :
[0,0];
const [size, setSize] = useState([elementWidth, elementHeight]);
useLayoutEffect(() => {
const updateSize = () => {
setSize([elementWidth, elementHeight]);
console.log(`elementWidth: ${elementWidth}px`);
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
},[]);
return size;
};
//carousel.ts
//
import { resizeWindow } from './resizeWindow.ts';
export const Carousel: FC = ({
children
}) => {
const parentRef = useRef<HTMLDivElement>(null);
const slideRef = createRef<HTMLDivElement>();
const [count, setCount ] = useState<number>(0);
const [parentWidth, setParentWidth] = resizeWindow(parentRef);
const total = React.Children.count(children);
const nextSlide = () => {
if( count < total -1 ){
setCount( count + 1 );
} else if( count === total-1 ){
setCount(1);
}
}
const prevSlide = () => {
if( count > 0 ){
setCount( count -1 );
} else if( count === 0 ){
setCount(total -1 );
}
}
useEffect(()=> {
console.log('parentRef: ', parentRef);
if(slideRef.current){
slideRef.current.style.transition = "all 0.5s ease-in-out";
slideRef.current.style.transform = `translateX(-${count}00%)`;
}
if(parentRef.current){
resizeWindow(parentRef);
}
},[count || parentWidth])
return(
<SliderContainer ref={parentRef}>
<Slider ref={slideRef} width={parentWidth * total}>
{children}
</Slider>
<Indicator now={1} total={total}/>
<Button onClick={prevSlide}>left</Button>
<Button onClick={nextSlide}>right</Button>
</SliderContainer>
)
}
resizeWindow is a custom hook, you should not be using it inside useEffect hook. This usage is what gives you an error.
Also you must name your custom hooks by prefixing their name with use
Also you must destructure ref properties within the updateSize function in resizeWindow hook so that you don't face the closure problem within updateSize function
The updated solution will look like
export const useResizeWindow: (ref: RefObject<HTMLElement>) => number[] = (ref) => {
const [size, setSize] = useState([elementWidth, elementHeight]);
useLayoutEffect(() => {
const updateSize = () => {
const [ elementWidth, elementHeight ] = ref.current ?
[ref.current.offsetWidth, ref.current.offsetHeight ] :
[0,0];
setSize([elementWidth, elementHeight]);
console.log(`elementWidth: ${elementWidth}px`);
};
updateSize();
window.addEventListener('resize', updateSize);
return () => window.removeEventListener('resize', updateSize);
},[]);
return size;
};
and its usage will be as follows
//carousel.ts
//
import { useResizeWindow } from './resizeWindow.ts';
export const Carousel: FC = ({
children
}) => {
const parentRef = useRef<HTMLDivElement>(null);
const slideRef = createRef<HTMLDivElement>();
const [count, setCount ] = useState<number>(0);
const [parentWidth, setParentWidth] = useResizeWindow(parentRef);
const total = React.Children.count(children);
const nextSlide = () => {
if( count < total -1 ){
setCount( count + 1 );
} else if( count === total-1 ){
setCount(1);
}
}
const prevSlide = () => {
if( count > 0 ){
setCount( count -1 );
} else if( count === 0 ){
setCount(total -1 );
}
}
useEffect(()=> {
console.log('parentRef: ', parentRef);
if(slideRef.current){
slideRef.current.style.transition = "all 0.5s ease-in-out";
slideRef.current.style.transform = `translateX(-${count}00%)`;
}
},[count || parentWidth])
return(
<SliderContainer ref={parentRef}>
<Slider ref={slideRef} width={parentWidth * total}>
{children}
</Slider>
<Indicator now={1} total={total}/>
<Button onClick={prevSlide}>left</Button>
<Button onClick={nextSlide}>right</Button>
</SliderContainer>
)
}