My code is causing an unexpected amount of re-renders.
function App() {
const [isOn, setIsOn] = useState(false)
const [timer, setTimer] = useState(0)
console.log('re-rendered', timer)
useEffect(() => {
let interval
if (isOn) {
interval = setInterval(() => setTimer(timer + 1), 1000)
}
return () => clearInterval(interval)
}, [isOn])
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
Note the console.log on line 4. What I expected is the following to be logged out:
re-rendered 0
re-rendered 0
re-rendered 1
The first log is for the initial render. The second log is for the re-render when the "isOn" state changes via the button click. The third log is when setInterval calls setTimer so it's re-rendered again. Here is what I actually get:
re-rendered 0
re-rendered 0
re-rendered 1
re-rendered 1
I can't figure out why there is a fourth log. Here's a link to a REPL of it:
https://codesandbox.io/s/kx393n58r7
***Just to clarify, I know the solution is to use setTimer(timer => timer + 1), but I would like to know why the code above causes a fourth render.
The function with the bulk of what happens when you call the setter returned by useState is dispatchAction within ReactFiberHooks.js (currently starting at line 1009).
The block of code that checks to see if the state has changed (and potentially skips the re-render if it has not changed) is currently surrounded by the following condition:
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
My assumption on seeing this was that this condition evaluated to false after the second setTimer call. To verify this, I copied the development CDN React files and added some console logs to the dispatchAction function:
function dispatchAction(fiber, queue, action) {
!(numberOfReRenders < RE_RENDER_LIMIT) ? invariant(false, 'Too many re-renders. React limits the number of renders to prevent an infinite loop.') : void 0;
{
!(arguments.length <= 3) ? warning$1(false, "State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().') : void 0;
}
console.log("dispatchAction1");
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdate = true;
var update = {
expirationTime: renderExpirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
var lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
flushPassiveEffects();
console.log("dispatchAction2");
var currentTime = requestCurrentTime();
var _expirationTime = computeExpirationForFiber(currentTime, fiber);
var _update2 = {
expirationTime: _expirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// Append the update to the end of the list.
var _last = queue.last;
if (_last === null) {
// This is the first update. Create a circular list.
_update2.next = _update2;
} else {
var first = _last.next;
if (first !== null) {
// Still circular.
_update2.next = first;
}
_last.next = _update2;
}
queue.last = _update2;
console.log("expiration: " + fiber.expirationTime);
if (alternate) {
console.log("alternate expiration: " + alternate.expirationTime);
}
if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
console.log("dispatchAction3");
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
var _eagerReducer = queue.eagerReducer;
if (_eagerReducer !== null) {
var prevDispatcher = void 0;
{
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
var currentState = queue.eagerState;
var _eagerState = _eagerReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
_update2.eagerReducer = _eagerReducer;
_update2.eagerState = _eagerState;
if (is(_eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
{
if (shouldWarnForUnbatchedSetState === true) {
warnIfNotCurrentlyBatchingInDev(fiber);
}
}
scheduleWork(fiber, _expirationTime);
}
}
and here's the console output with some additional comments for clarity:
re-rendered 0 // initial render
dispatchAction1 // setIsOn
dispatchAction2
expiration: 0
dispatchAction3
re-rendered 0
dispatchAction1 // first call to setTimer
dispatchAction2
expiration: 1073741823
alternate expiration: 0
re-rendered 1
dispatchAction1 // second call to setTimer
dispatchAction2
expiration: 0
alternate expiration: 1073741823
re-rendered 1
dispatchAction1 // third and subsequent calls to setTimer all look like this
dispatchAction2
expiration: 0
alternate expiration: 0
dispatchAction3
NoWork has a value of zero. You can see that the first log of fiber.expirationTime after setTimer has a non-zero value. In the logs from the second setTimer call, that fiber.expirationTime has been moved to alternate.expirationTime still preventing the state comparison so re-render will be unconditional. After that, both the fiber and alternate expiration times are 0 (NoWork) and then it does the state comparison and avoids a re-render.
This description of the React Fiber Architecture is a good starting point for trying to understand the purpose of expirationTime.
The most relevant portions of the source code for understanding it are:
ReactFiberExpirationTime.js
ReactFiberScheduler.js
I believe the expiration times are mainly relevant for concurrent mode which is not yet enabled by default. The expiration time indicates the point in time after which React will force a commit of the work at the earliest opportunity. Prior to that point in time, React may choose to batch updates. Some updates (such as from user interactions) have a very short (high priority) expiration, and other updates (such as from async code after a fetch completes) have a longer (low priority) expiration. The updates triggered by setTimer from within the setInterval callback would fall in the low priority category and could potentially be batched (if concurrent mode were enabled). Since there is the possibility of that work having been batched or potentially discarded, React queues a re-render unconditionally (even when the state is unchanged since the previous update) if the previous update had an expirationTime.
You can see my answer here to learn a bit more about finding your way through the React code to get to this dispatchAction function.
For others who want to do some digging of their own, here's a CodeSandbox with my modified version of React:
The react files are modified copies of these files:
https://unpkg.com/react#16/umd/react.development.js
https://unpkg.com/react-dom#16/umd/react-dom.development.js
Related
I'm trying to change my page's content when clicking on the link but the thing is it only changes on reload and not directly when clicked.
function Home( { category } ) {
const [news, setNews] = useState([]);
useEffect(() => {
async function newsArticles(){
const request = await axios.get(category);
console.log(request.data.articles);
setNews(request.data.articles);
return request;
}
newsArticles();
}, [category]);
return (
<div className='home'>
{news.map(ns => (
<NewsCard className='homeNewsCard' key = {ns.source.id} title = {ns.title} description = {ns.description} image = {ns.urlToImage} link = {ns.url}/>
))}
</div>
)
}
Since the effect captures the values at the time the component is rendered, any asynchronous code that executed in the effect will also see these values rather than the latest values. Example :
useEffect(()=>{
asyncFunc(props.val)
.then((res)=> setData(res + props.oth))
},[props.val]);
props.oth is used when the promise resolves, but the effecct captures the values of props.val and prips.oth at the time of render , the value of props.oth had changed during the time it took for the promise to reoslve , the code would see the old value
Solution :
you can either use reference to store the value of props.oth so your code will always see the latest value or you can add props.oth back to dependency array and return a cleanup function .
Example :
useEffect(()=>{
let cncl = false
asyncFunc(props.val)
.then((res)=>{
if(!cncl)
setData(res + props.oth)})
return ()=> cncl = true}
,[props.val, props.oth])
cncl flag is raised before running the effecct again (when props.val or props.oth have changes comapred with the previous render).
This is the backend node.js route
app.post('/analysis' , async (req , res) => {
const {temp , humidity} = req.body.data.current
let predictedCrops:any[] = []
let humidityDiffernece1
let humidityDiffernece2
let tempDifference1
let tempDifference2
//Getting all crops and optimums from databse so they are in an array (Easier to work with)
const data = await CropOptimum.find({})
data.map(({name , min_optimum_temp , max_optimum_temp , min_optimum_humidity , max_optimum_humidity }) => {
if(temp >= min_optimum_temp && temp <= max_optimum_temp && humidity >= min_optimum_humidity && humidity <= max_optimum_humidity) {
const data = {
name: name,
msg: 'Perfect Temperature and Humidity for this crop'
}
predictedCrops.push(data)
// console.log(`Perfect Match ${name}`)
} else if (temp >= min_optimum_temp && temp <= max_optimum_temp && humidity <= min_optimum_humidity) {
humidityDiffernece1 = min_optimum_humidity - humidity
// console.log(`Humidity is less than minimum and difference is ${humidityDiffernece1}`)
} else if (temp >= min_optimum_temp && temp <= max_optimum_temp && humidity >= max_optimum_humidity ) {
humidityDiffernece2 = humidity - max_optimum_humidity
// console.log(`Humidity is more than maximum and difference is ${humidityDiffernece2}`)
} else if(humidity >= min_optimum_humidity && humidity <= max_optimum_humidity && temp <= min_optimum_temp) {
tempDifference1 = min_optimum_temp - temp
// console.log(`Temperature is less than minimum , difference is ${tempDifference1}`)
} else if(humidity >= min_optimum_humidity && humidity <= max_optimum_humidity && temp >= max_optimum_temp) {
tempDifference2 = temp - max_optimum_temp
// console.log(tempDifference2)
} else {
// console.log("NO MATCHES")
}
})
console.log(predictedCrops)
res.send(predictedCrops)
})
This is where I call the API or make the request to /analysis in react frontend
useEffect( () => {
navigator.geolocation.getCurrentPosition((postion) => {
setLatitiude(postion.coords.latitude)
setLongitude(postion.coords.longitude)
})
getWeatherDetails()
}, [isAnalyzing , predictedCrop])
This is the getWeatherDetails function
const getWeatherDetails = async() => {
try {
const response = await axios.get(`${API_ENDPOINT}lat=${latitude}&lon=${longitude}&exclude=hourly,daily&units=metric&appid=${API_KEY}`)
console.log(response.data)
//Send temp and humidity to backend for analysis
const dataToSend = response.data
const sentWeatherData = await axios.post("http://localhost:4000/analysis" , {
data: dataToSend
}).then((response) => {
let dataObject = response.data;
setPredictedCrop(dataObject)
console.log(predictedCrop)
})
} catch {
console.log("error")
}
}
So basically, what is happening is that when a button is clicked it sets isAnalyzing state to true and the process starts. When I remove predictedCrop from the dependency it does not cause the loop however the predictedCrop state will not update on the line where it says setPredictedCrop(dataObject). When I add the predictedCrop to the dependency of the useEffect it causes the loop but sets the state successfully.
I am kind of a beginner at this so any help is appreciated, thank you.
When I remove predictedCrop from the dependency it does not cause the loop however the predictedCrop state will not update on the line where it says setPredictedCrop(dataObject). When I add the predictedCrop to the dependency of the useEffect it causes the loop but sets the state successfully.
You got the wrong concept here. Mentioning a state variable or a prop in a dependency array is not a necessity for the state variable or a prop value to change. It denotes that any changes to that value will trigger the useEffect.
In your case, if predictedCrop, is not in the dependency array, it's state will change, it's just that, it won't trigger the useEffect, whenever it's updated.
Putting it in the array, will trigger the useEffect and will cause infinite loop.
The reason the console.log's are empty is because react state setting is asynchronous. It's probably setting it fine, it just happens shortly after the console.log because of how React batches state updates and runs them later seamlessly.
I would suggest logging it in the render method or something.
See here for more details: https://beta.reactjs.org/apis/usestate#ive-updated-the-state-but-logging-gives-me-the-old-value.
So removing predictedCrop from deps array is fine, its just your logging is confusing you :).
Its worth noting however you probably should not be doing the network call in the useEffect anyway. It would be better to do it in the event handler of the button itself. useEffect is for side effects, and this isnt one -- its in response to button click.
See here https://beta.reactjs.org/learn/synchronizing-with-effects#not-an-effect-buying-a-product
You wouldn’t want to buy the product twice. However, this is also why you shouldn’t put this logic in an Effect. What if the user goes to another page and then presses Back? Your Effect would run again. You don’t want to buy the product when the user visits a page; you want to buy it when the user clicks the Buy button.
I have a React video player project and I have a way of skipping the video by 1 second when a keyDown event occurs. This is effective because if you hold the key down multiple events are fired so the video continues to skip at e.g. 5-second intervals up until the user releases the key.
I now want to implement it differently so when a user taps the key down once and then, even if they lift their finger the video carries on skipping until the user presses the key down once again (so they don't need to hold the key down)
How do I do this? This is a section of my code so far. It's actually for a Smart TV APP so any reference you read regards focus is just the navigation pattern for selecting divs with your TV remote. The main bit of code is inside the keyDown function that then calls handleSkipForward or handleSkipBack. Also the videoElement ref is shared across functional components using store.VideoElement.
I was thinking of something like having a boolean state hook for skipping but when that state is true how do I repeatedly request a skip of 5 seconds or is there something within the video element that you can just set to progress through a video at a set rate?
focusKey,
elementRef: containerRef,
onKeyDown: (event) => {
// if (!playerControlsVisible) {
// event.stopPropagation()
// event.preventDefault()
// return
// }
if (event.key === "Enter" || event.key === " ") {
skipDirection === "Forward"
? handleSkipForwardAction()
: handleSkipBackwardAction()
}
},
onFocus: (event) => {},
})
function handleSkipForwardAction() {
if (store.videoElement.currentTime + skipTime < duration) {
store.videoElement.currentTime += skipTime
} else if (store.videoElement.currentTime < duration) {
store.videoElement.currentTime = duration
} else return
}
function handleSkipBackwardAction() {
if (store.videoElement.currentTime - skipTime > 0) {
store.videoElement.currentTime -= skipTime
} else if (store.videoElement.currentTime > 0) {
store.videoElement.currentTime = 0
} else return
}```
It should be simple. What you need to do is implement the setInterval function.
You can just add an interval (infinite loop) and store the Interval ID on a state so that you can stop that infinite loop using the clearInterval function.
onKeyDown: (event) => {
if (event.key === "Enter" || event.key === " ") {
if (moveForward){
clearInterval(moveForward);
// Assuming you use Functional Component and `useState` hooks.
setMoveForward(null);
return;
}
const action = skipDirection === "Forward"
? handleSkipForwardAction
: handleSkipBackwardAction;
setMoveForward(setInterval(action,100));
// 100 is in milisecond. so 1000 = 1 second.
// 100 = 10 function call in 1 second. Meaning 6 second skip/second.
// You can adjust the calculation on your own.
}
},
This is not tested, so do let me know if it works or not. But the idea is to use a looping function when the user clicks the specific button and stop the looping function when the user clicks the specific button again.
References:
setInterval
clearInterval
I am developing a simple Battleships board game and need to increment a state variable when a player clicks on a cell with a valid move.
CodeSandbox: https://codesandbox.io/s/trusting-leakey-6ek9o
All of the code pertaining to this question is found within the following file: >src>GameboardSetup>GameboardSetup.js
The state variable in question is called placedShips. It is designed to keep track of how many ships have been placed onto the board. The variable is initialised to 0 and is supposed to increment up until it reaches a set integer value:
const [placedShips, setPlacedShips] = useState(0);
When the user clicks on the grid, an onClick handler fires. If the cell was a valid cell, then a ship should be placed into an array and the value of placedShips should increment. When the value increments, a new ship is then selected to be placed on a subsequent click. Each new ship has a different length.
Currently, when the user clicks on a valid grid cell, a ship is correctly placed into the array. The issue arises on subsequent valid clicks, whereby the same ship is then placed into the array. This issue is being driven by the onClick handler apparently not receiving an updated state value for placedShips.
While I can see from a { useEffect } hook that the state is in fact increment, the placedShips variable within the event handler I believe is constantly set to 0. Below is how I believe I have validated this issue.
Here is the onClick event handler, containing a console log for the state variable:
const onClickHandler = (e) => {
console.log(placedShips)
let direction = ships[placedShips].direction;
let start = parseInt(e.target.id);
let end = start + ships[placedShips].length - 1;
console.log(playerGameboard.checkIfShipPresent());
if ((playerGameboard.checkValidCoordinates(direction, start, end)) && (!playerGameboard.checkIfShipPresent(direction, start, end))) {
playerGameboard.placeShip(placedShips, direction, start, end);
setPlacedShips(oldValue => oldValue + 1);
}
}
and here is the { useEffect } hook with another console log for the state variable:
useEffect(() => {
console.log(placedShips)
}, [placedShips])
By selecting multiple valid cells, this is the console log output:
GameboardSetup.js:70 0 <--- Event handler console log
GameboardSetup.js:74 false
GameboardSetup.js:143 1 <--- useEffect console log
GameboardSetup.js:70 0
GameboardSetup.js:74 false
GameboardSetup.js:143 2
GameboardSetup.js:70 0
GameboardSetup.js:74 false
GameboardSetup.js:143 3
You can see that the event handler console log always reports placedShips as 0. Whereas the { useEffect } hook shows it incrementing with each successive click.
Apologies for the longwinded question, I have been stuck on this problem for 2 weeks now and haven't made any significant progress with it. Any advice would be hugely appreciated.
Thank you!
Your onClickHandle is not updated when the UI Render. Move it into your return. and it works fine. No need to use humanSetUpGrid. Just get the result from createUiGrid. So cells will be updated when app render.
const createUiGrid = () => {
const cells = [];
for (let i = 0; i < 100; i++) {
cells.push(i);
}
let counter = -1;
const result = cells.map((cell) => {
counter++;
return (
<div
className="cell"
id={counter}
onClick={onClickHandler}
onMouseOut={onMouseOutHandler}
onMouseOver={onMouseOverHandler}
/>
);
});
return result;
};
<div className="setup-grid">
<Table grid={createUiGrid()} />
</div>
I have a form that contains several questions. Some of the questions contains a group of subquestions.
The logic to render sub questions is written inside componentDidUpdate method.
componentDidUpdate = (prevProps, prevState, snapshot) => {
if (prevProps !== this.props) {
let questions = this.props.moduleDetails.questions,
sgq = {};
Object.keys(questions).map((qId) => {
sgq[qId] = (this.state.groupedQuestions[qId]) ? this.state.groupedQuestions[qId] : [];
let answerId = this.props.formValues[qId],
questionObj = questions[qId],
groupedQuestions = [];
if(questionObj.has_grouped_questions == 1 && answerId != null && (this.state.groupedQuestions != null)) {
groupedQuestions = questions[qId].question_group[answerId];
let loopCount = this.getLoopCount(groupedQuestions);
for(let i=0; i<loopCount; i++) {
sgq[qId].push(groupedQuestions);
}
}
});
this.setState({groupedQuestions: sgq});
}
}
The problem is that on every key stroke of text field, handleChange method is invoked which will ultimately invoke componentDidUpdate method. So the same question groups gets rendered on every key stroke.
I need a way to detect if the method componentDidUpdate was invoked due to the key press(handleChange) event so that i can write logic as follows.
if(!handleChangeEvent) {
Logic to render question group
}
Any idea on how to integrate this will be appreciated.
I assume your textfield is a controlled component, meaning that its value exists in the state. If this is the case, you could compare the previous value of your textfield to the new one. If the value is different, you know the user entered something. If they are equal however, the user did something else at which point you want your snippet to actually execute.
Basically:
componentDidUpdate = (prevProps) => {
// if value of textfield didn't change:
if (prevProps.textfieldValue === this.props.textfieldValue) {
// your code here
}
}
Another approach is to use componentDidReceiveProps(). There you can compare the props to the previous ones, similarly to the above, and execute your code accordingly. Which method is most suitable depends on how your app works.