I'm writing a code where I need to get Min and Max values. In my real-time scenario, I've got an SDK to which I make two promise calls and it returns min and max values. I'm trying to mock up the same in my example below. I've got a context in place that is used to store the values. Here is my sample code.
Here is my context:
import React, { useState } from "react";
import { useContext } from "react";
const ProductsContext = React.createContext();
export const ProductsProvider = ({ children }) => {
const [priceValues, setPriceValues] = useState([0, 0]);
return (
<ProductsContext.Provider
value={{
priceValues,
setPriceValues
}}
>
{children}
</ProductsContext.Provider>
);
};
export const useProductsContext = () => {
return useContext(ProductsContext);
};
And my code is as below:
import { useEffect } from "react";
import { PriceRange } from "./PriceRange";
import { useProductsContext } from "./ProductsContext";
const ShowRange = () => {
const { priceValues, setPriceValues } = useProductsContext();
const getMinVal = () => {
setPriceValues([Math.random() * 100, priceValues[1]]);
};
const getMaxVal = () => {
setPriceValues([priceValues[0], Math.random() * 100]);
};
useEffect(() => {
getMinVal();
getMaxVal();
}, []);
console.log(JSON.stringify(priceValues));
return <>{priceValues[0] && priceValues[1] && <PriceRange />}</>;
};
export default ShowRange;
Currently, when I run this, priceValues is of format [0, randomNumber]. And once I get the values.
I'm confused on why only the second value in Array gets updated but not the first. Where am I going wrong?
Here is working code of the same.
Even though getMinVal and getMaxVal are separate functions that update state, they are called at the same time. This is causing the first update to be lost by the second update since priceValues[1] will not be updated until the next render.
You could solve this by using the function update form of setting state:
const getMinVal = () => {
setPriceValues((prev) => ([Math.random() * 100, prev[1]]));
};
const getMaxVal = () => {
setPriceValues((prev) => ([prev[0], Math.random() * 100]));
};
After you call setPriceValues React sets value (internally) and schedule re-render, but value stored in priceValues const is not updated.
State is not exactly like variable. State has same value during one re-render. If you want actual value during re-render (after mutation) pass function with param to setState.
You code with expecting behaviour:
const ShowRange = () => {
const { priceValues, setPriceValues } = useProductsContext(); // priceValues == [0, 0]
const getMinVal = () => {
// priceValues == [0, 0]
setPriceValues((pricesValues) => [Math.random() * 100, priceValues[1]]);
};
const getMaxVal = () => {
// priceValues == [0, 0]
setPriceValues((priceValues) => [priceValues[0], Math.random() * 100]);
};
useEffect(() => {
// priceValues == [0, 0]
getMinVal();
// priceValues == [0, 0]
getMaxVal();
}, []);
console.log(JSON.stringify(priceValues));
return <>{priceValues[0] && priceValues[1] && <h1>Hi</h1>}</>;
};
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 am a beginner using useEffect in React to update the index state of an array on an interval. I want the index to increase by 1 every five seconds, and when it reaches the end of the array loop back to 0.
I put an 'if' statement to check for this within my useEffect function, but it doesn't seem to be firing. When the array reaches the end, my console shows an error reading from an undefined index.
const [idx, setIdx] = useState(0);
useEffect(() => {
const interval = setInterval(() => setIdx((previousValue) => previousValue
+1), 5000);
if(idx > testimonials.length-1) { setIdx(0)};
return () => {
clearInterval(interval);
};
}, []);
Can anyone see what I'm doing wrong?
Use the remainder operator (%):
const [idx, setIdx] = useState(0);
useEffect(() => {
const interval = setInterval(
() => setIdx(idx => (idx + 1) % testimonials.length),
5000,
);
return () => clearInterval(interval);
}, []);
You need to pass the idx value to the useEffect dependencies array.
import React from 'react';
const testimonials = [1, 2, 3, 4, 5, 6];
export default function App() {
const [idx, setIdx] = React.useState(0);
React.useEffect(() => {
const interval = setInterval(
() => setIdx((previousValue) => previousValue + 1),
1000
);
if (idx > testimonials.length - 1) {
setIdx(0);
}
return () => {
clearInterval(interval);
};
}, [idx]);
return (
<div>
<h1>{idx}</h1>
</div>
);
}
you should use this statement
if(idx > testimonials.length-1) { setIdx(0)};
outside the useEffect and above the return which is returning the JSX.
I need the useProductList function to execute and finish all process before randomProduct function will execute.
For some reason it doesnt work when fetchData is a Promise so randomProduct wont be executed.
I even tried without Promise, nothing did work.
my custom hook
import { useState, useEffect } from "react";
export default function useProductList() {
const [productList, setProductObjsList] = useState([]);
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
let randomProducts = [];
const fetchData = () =>
new Promise(() => {
arr.splice(1, 7);
setProductObjsList(arr);
return arr;
});
const randomProduct = (productArr) => {
//some Math.random() and algorithm with the productArr
console.log("randomProduct()", productArr);
};
useEffect(() => {
fetchData().then((result) => randomProduct(result));
}, []);
return randomProducts;
}
CodeSandbox
I will be glad if someone will open my eyes and show me the right way of how to do it.
EDIT:
My original fetchData function
const fetchData = () =>
{
fetch('http://localhost:49573/WebService.asmx/ProductList')
.then((response) => response.text())
.then(
(xml) =>
new window.DOMParser().parseFromString(xml, 'text/xml')
.documentElement.firstChild.textContent
)
.then((jsonStr) => JSON.parse(jsonStr))
.then((data) => {
setProductObjsList(data);
})
});
randomProduct function
```const randomProduct = (productObjectList) => {
const products = [...productObjectList];
for (let index = 0; index < products.length; index++) {
let idx = Math.floor(Math.random() * products.length);
randomProducts.push(products[idx]);
products.splice(idx, 1);
}
};```
It's unclear what your exact intentions are, but from the name useRandomProduct I'm going to make a guess. Top things I'd like for you to take from this answer -
You cannot mutate state in React. You cannot use Array.prototype.pop and expect your React components to work correctly. If a value is meant to change over the lifetime of the component, use useState.
Don't put all of your functions inside of the hook. This tendency probably comes from class-oriented thinking but has no place in functional paradigm. Functions can be simple and do just one thing.
import { useState, useEffect } from "react"
// mock products
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
// mock fetch
const fetchData = () =>
new Promise(resolve => {
resolve(arr)
})
// random picker
function choose1(all) {
const i = Math.floor(Math.random() * all.length)
return all[i]
}
export default function useRandomProduct() {
// state
const [product, setProduct] = useState(null)
// effect
useEffect(async () => {
// fetch products
const products = await fetchData()
// then choose one
setProduct(choose1(products))
}, [])
// return state
return product
}
To use your new hook -
import { useRandomProduct } from "./useRandomProduct.js"
import Product from "./Product.js"
function MyComponent() {
const product = useRandomProduct()
if (product == null)
return <p>Loading..</p>
else
return <Product {...product} />
}
Full demo -
const { useState, useEffect } = React
// useRandomProduct.js
const arr = [{name:"apple"},{name:"carrot"},{name:"pear"},{name:"banana"}]
const fetchData = () =>
new Promise(r => setTimeout(r, 2000, arr))
function choose1(all) {
const i = Math.floor(Math.random() * all.length)
return all[i]
}
function useRandomProduct() {
const [product, setProduct] = useState(null)
useEffect(() => {
fetchData().then(products =>
setProduct(choose1(products))
)
}, [])
return product
}
// MyComponent.js
function MyComponent() {
const product = useRandomProduct()
if (product == null)
return <p>Loading..</p>
else
return <div>{JSON.stringify(product)}</div>
}
// index.js
ReactDOM.render(<div>
<MyComponent />
<MyComponent />
<MyComponent />
</div>, document.querySelector("#main"))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
<div id="main"></div>
The problem is you are not resolving the promise. So, until the promise is resolved it will not go inside then() block.
Here's what you have to do
const fetchData = () =>
new Promise((resolve,reject) => {
arr.pop(1, 7);
setProductObjsList(arr);
resolve(arr);
});
A promise has three stage.
Pending
Resolved
Rejected
In your example the promise is always in pending state and never goes to the Resolved state. Once, a promise is resolved it moves to then() block and if it is rejected it moves to catch() block.
React state value not updated in the console but it is updated in the view.
This is my entire code
import React, { useEffect, useState } from 'react';
const Add = (props) => {
console.log("a = ", props.a)
console.log("b = ", props.b)
const c = props.a+props.b;
return (
<div>
<p><b>{props.a} + {props.b} = <span style={{'color': 'green'}}>{c}</span></b></p>
</div>
)
}
// export default React.memo(Add);
const AddMemo = React.memo(Add);
const MemoDemo = (props) => {
const [a, setA] = useState(10)
const [b, setB] = useState(10)
const [i, setI] = useState(0);
useEffect(() => {
init()
return () => {
console.log("unmounting...")
}
}, [])
const init = () => {
console.log("init", i)
setInterval(()=>{
console.log("i = ", i)
if(i == 3){
setA(5)
setB(5)
}else{
setA(10)
setB(10)
}
setI(prevI => prevI+1)
}, 2000)
}
return (
<div>
<h2>React Memo - demo</h2>
<p>Function returns previously stored output or cached output. if inputs are same and output should same then no need to recalculation</p>
<b>I= {i}</b>
<AddMemo a={a} b={b}/>
</div>
);
}
export default MemoDemo;
Please check this image
Anyone please explain why this working like this and how to fix this
The problem is as you initialized the setInterval once so it would reference to the initial value i all the time. Meanwhile, React always reference to the latest one which always reflect the latest value on the UI while your interval is always referencing the old one. So the solution is quite simple, just kill the interval each time your i has changed so it will reference the updated value:
React.useEffect(() => {
// re-create the interval to ref the updated value
const id = init();
return () => {
// kill this after value changed
clearInterval(id);
};
// watch the `i` to create the interval
}, [i]);
const init = () => {
console.log("init", i);
// return intervalID to kill
return setInterval(() => {
// ...
});
};
In callback passed to setInterval you have a closure on the value of i=0.
For fixing it you can use a reference, log the value in the functional update or use useEffect:
// Recommended
useEffect(() => {
console.log(i);
}, [i])
const counterRef = useRef(i);
setInterval(()=> {
// or
setI(prevI => {
console.log(prevI+1);
return prevI+1;
})
// or
conosole.log(counterRef.current);
}, 2000);
I've got some components which need to render sequentially once they've loaded or marked themselves as ready for whatever reason.
In a typical {things.map(thing => <Thing {...thing} />} example, they all render at the same time, but I want to render them one by one I created a hook to to provide a list which only contains the sequentially ready items to render.
The problem I'm having is that the children need a function in order to tell the hook when to add the next one into its ready to render state. This function ends up getting changed each time and as such causes an infinite number of re-renders on the child components.
In the examples below, the child component useEffect must rely on the dependency done to pass the linter rules- if i remove this it works as expected because done isn't a concern whenever it changes but obviously that doesn't solve the issue.
Similarly I could add if (!attachment.__loaded) { into the child component but then the API is poor for the hook if the children need specific implementation such as this.
I think what I need is a way to stop the function being recreated each time but I've not worked out how to do this.
Codesandbox link
useSequentialRenderer.js
import { useReducer, useEffect } from "react";
const loadedProperty = "__loaded";
const reducer = (state, {i, type}) => {
switch (type) {
case "ready":
const copy = [...state];
copy[i][loadedProperty] = true;
return copy;
default:
return state;
}
};
const defaults = {};
export const useSequentialRenderer = (input, options = defaults) => {
const [state, dispatch] = useReducer(options.reducer || reducer, input);
const index = state.findIndex(a => !a[loadedProperty]);
const sliced = index < 0 ? state.slice() : state.slice(0, index + 1);
const items = sliced.map((item, i) => {
function done() {
dispatch({ type: "ready", i });
return i;
}
return { ...item, done };
});
return { items };
};
example.js
import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";
import { useSequentialRenderer } from "./useSequentialRenderer";
const Attachment = ({ children, done }) => {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
const delay = Math.random() * 3000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("happening multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [done]);
return <div>{loaded ? children : "loading"}</div>;
};
const Attachments = props => {
const { items } = useSequentialRenderer(props.children);
return (
<>
{items.map((attachment, i) => {
return (
<Attachment key={attachment.text} done={() => attachment.done()}>
{attachment.text}
</Attachment>
);
})}
</>
);
};
function App() {
const attachments = [1, 2, 3, 4, 5, 6, 7, 8].map(a => ({
loaded: false,
text: a
}));
return (
<div className="App">
<Attachments>{attachments}</Attachments>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Wrap your callback in an aditional layer of dependency check with useCallback. This will ensure a stable identity across renders
const Component = ({ callback }) =>{
const stableCb = useCallback(callback, [])
useEffect(() =>{
stableCb()
},[stableCb])
}
Notice that if the signature needs to change you should declare the dependencies as well
const Component = ({ cb, deps }) =>{
const stableCb = useCallback(cb, [deps])
/*...*/
}
Updated Example:
https://codesandbox.io/s/wizardly-dust-fvxsl
Check if(!loaded){.... setTimeout
or
useEffect with [loaded]);
useEffect(() => {
const delay = Math.random() * 1000;
const timer = setTimeout(() => {
setLoaded(true);
const i = done();
console.log("rendering multiple times", i, new Date());
}, delay);
return () => clearTimeout(timer);
}, [loaded]);
return <div>{loaded ? children : "loading"}</div>;
};