useState is not changing it's value on useEffect hook [duplicate] - reactjs

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed 2 months ago.
I am trying to animate a header on a personal site I'm working on.
It's a simple fade in fade out effect as the user scrolls through the page.
To get this effect what i'm doing is using an Intersection Observer to check wether or not a certain element is in view or not.
When that element is in view, i'd like to change the state elementInView to reflect that or not using a boolean value.
when I console.log the isIntersecting value of the element that the observer is listening to, I can see that it changes True or False based on whether the element is in view or not.
so... since that is working, i'm using that value to set state of elementInView.
BUT!!! the state is not changing when the element is in view, even though it should. I'm not sure what went wrong but I cannot figure it out for the life of me.
here's the code:
import React from 'react';
import { useEffect, useRef, useState } from 'react';
import { Link, useRouteMatch, useNavigate } from 'react-router-dom';
import BoulderImage from "../data/nat_pic_boulders_and_tree.jpeg";
import FenceImage from "../data/nat_pic_fence.jpeg";
import GrassField from '../data/nat_pic_grass.jpeg';
export default function HomePage() {
const navigate = useNavigate();
const firstRef = useRef();
const secondRef = useRef();
const [elementInView, setElementInView] = useState();
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
setElementInView(entry.isInteresecting);
console.log(elementInView);
})
observer.observe(firstRef.current);
observer.observe(secondRef.current);
}, [elementInView])
return (
<div>
<h1 className="main-intro-header">Welcome Home</h1>
<div className='nav-links'>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</div>
<div className="stop-point-one" ref={firstRef}>stop point</div>
<div className="stop-point-two" ref={secondRef}>stop point</div>
</div>
)}
the list at the end of the useEffect hook. i've tried putting elementInView in there and taking it out and re-running it. no luck.

I think that the thing you are missing is that after calling setElementInView it does not change elementInView on the current render. Only in the next render for that component elementInView will get the new state. And that way you are getting the wrong output to the console.
The way to solve it is to add another useEffect hook with elementInView as a dependency and do you logic there.
You can watch a great video of Jack Herrington regarding this:
https://www.youtube.com/watch?v=RAJD4KpX8LA

Henrys comment links to a better explanation than I can give. Summed up, since setState is performed in an async nature, the value will not be what you set it to immediately after the setState line is ran. If you move your console.log line out of your observer declaration, into the body of the useEffect, it should work as you expect. I put together a little sandbox and spaced some elements apart to put them off screen to show this as well.
https://codesandbox.io/s/observer-test-m5qh88?file=/src/App.js
If you scroll the elements on and off the page with the console open, you'll see it update each time they move off and back onto the screen.

I figured it out!
I saw a simpler way to set the state of these elements as they appear on the screen.
i updated this piece of code:
const [elementInView, setElementInView] = useState();
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
setElementInView(entry.isInteresecting);
console.log(elementInView);
})
observer.observe(firstRef.current);
observer.observe(secondRef.current);
}, [elementInView])
to this:
import { useInView } from 'react-intersection-observer';
const { ref: firstRef, inView: firstBreakIsVisibile} = useInView();
const { ref: secondRef, inView: secondBreakIsVisibile} = useInView();
I used npm to install the react-intersection-observer dependency.

Related

How to properly handle useStates and useEffects in a Reactjs simple quiz app that is rendering everything twice?

I'm building this simple quiz app in React where a random Korean hangul from a 40-item Data.json is displayed on the screen and users have 5 buttons with different answers which also come from Data.json.
For clarification, this is my application and the components I'm using. All components are siblings and live inside App.js.
What the application is suppose to do is to create a 5-item random array and select one of these as the right answer before render. If the user taps the right button all the components should be re-rendered with different items. That's why I decided to use the useEffect() hook.
But what is currently happening is:
Every console.log is being displayed twice in the console. The first time variables are empty and in the second time they display the values they should have;
The randomOptions array has 10 items instead of 5;
If the <Question/> component is in the code then I get errors and console.log tells me the values from my useState hooks are undefined. If <Question/> is removed I get the console shown in the picture below.
Console for clarification:
App.js component:
import './App.css';
import Data from './data/data.json';
import Header from './components/Header';
import Question from './components/Question';
import { useState, useEffect } from 'react';
function App() {
const [index, setIndex] = useState(Math.floor(Math.random() * 5)); // Tells us what the index of the right answer is
const [randomOptions, setRandomOptions] = useState([]); // Random array with 5 items from Data.json that are the options
const [question, setQuestion] = useState([]); // The question that goes to <Question/> and people need to guess
const randomizeButtonAnswer = () => {
let previousArray = randomOptions;
setRandomOptions([]);
for (let i=0; i < 5; i++){
let randomNumber = Math.floor(Math.random() * Data.length);
if (!randomOptions.includes(randomNumber) && !previousArray.includes(randomNumber)) {
setRandomOptions(randomOptions.push(randomNumber));
} else {
i--;
}
}
setRandomOptions(randomOptions.sort(() => Math.random() - 0.5));
setQuestion(randomOptions[index]);
}
useEffect(() => {
randomizeButtonAnswer();
}, [index])
console.log('index is ' + index); // 3
console.log('randomOptions is ' + randomOptions); // 33,9,3,26,8,20,0,12,29,25
console.log('question is ' + question); //26
return (
<div className="App">
<Header />
<Question answer={question}/>
</div>
);
}
export default App;
Question.js component
import './Question.css';
import Data from '../data/data.json';
const Question = ({ answer }) => {
return (
<div className="question-wrapper">
<h1 className="question"><span className="highlight">{Data[answer].character}</span></h1>
<span>{Data[answer].character} = {Data[answer].char_id} </span>
</div>
);
}
export default Question;
I'm completely lost here. Not sure why things are being rendered twice or why when present <Question/> makes my variables become undefined. I assume something inside randomizeButtonAnswer is broken but I've been unable to find what exactly.
Small disclaimer: I'm a complete noob to React so if I'm doing something that is wrong or a bad practice do let me know.
Thanks for your time!
I tried to fix your code but it's complicated and wrong at many points so I decided to write the function myself. Sorry about that.
Here is the codesandbox
there are many problem with your code.
About log 2 times. It is <StrictMode />
You should understand more about React State. It not change right in the place you write it, it's queue up and update when your functions end. set 1 state 2 time in 1 function does not mean anything.
Don't do i-- in for loop like that which is complicated to understand how things goes and hard to debug. use while loop or for best just don't. check the condition and log out or have some handle for it.
Wanna log? log inside React Component? use useEffect.
Default state of Question is not [].
Always do conditional render if default value of state is possible be either null or undefined.
Good luck with your React journey.

Does setState function of useState hook trigger re-render of whole component or just the returned JSX part of the code?

I am a beginner in React and I am learning it from a udemy course. Initially I thought the whole code inside the functional component gets re rendered/re run after state update. But then I saw this implementation of countdown before redirect and got confused.
import React, { useState, useEffect } from "react";
import { useHistory } from "react-router-dom";
const LoadingToRedirect = () => {
const [count, setCount] = useState(5);
let history = useHistory();
useEffect(() => {
const interval = setInterval(() => {
setCount((currentCount) => --currentCount);
}, 1000);
// redirect once count is equal to 0
count === 0 && history.push("/");
// cleanup
return () => clearInterval(interval);
}, [count]);
return (
<div className="container p-5 text-center">
<p>Redirecting you in {count} seconds</p>
</div>
);
};
export default LoadingToRedirect;
So why is setInterval needed here if setCount triggers a re-render of the whole code? Can't we do the same thing with setTimeout? So I tried that and it worked. Surely there is a reason he did it that way? Am I missing something?
Of course React re-renders the whole component but it also depends on some conditions. For example if you look at your code you have passed count variable as a dependency to useEffect hook, it means if the value of count changes React will render the effect inside of the useEffect hook.
Yes, you can achieve the same using setTimeout;setInterval is
pointless because it totally depends on count variable you passed as a
dependency.
if you remove count as a dependency then you can easily see it will
not redirect you the required page.

React update useState() when variable value changes

How to update UseState() when variable 'matchUrl' changes. when navigating to another route the 'matchUrl' value changes. and the changes is reflected on console.log and on the 'h1' tag. but not the usestate(), it keeps the old value.
import React, {useState} from 'react'
import useInfinityScroll from './hooks/useInfinityScroll'
import names form './data/names'
function TableList({ match }) {
let matchUrl = match.params.name
const dataMatch = names.filter(name => name.occupation === matchUrl)
const [listNames, setListNames] = useState(dataMatch.slice(0, 10))
const [isFetching, setIsFetching] = useInfinityScroll(fetchMoreListItems) //fetches more items when scrolling to bottom of page
function fetchMoreListItems() {
setTimeout(() {
setListNames([...dataMatch.slice(0, 20)])
setIsFetching(false)
}, 2000)
}
return (
<>
<h1>{matchUrl}</h1>
<div>
{listNames.filter(name => name.occupation === matchUrl).map(listNames => <Table key={listNames.id} name={listNames} />)}
</div>
</>
)
}
export default TableList
useState hook takes initial value of the state as an argument, but it only assign the initial value to the state at the component mounting phase, not during re-rendering of the component if anything changes. Therefore, after initial rendering, state won't be changed even if the dataMatch variable changes.
Therefore, you need to have useEffect hook having dependency on dataMatch variable to keep track on its changes. Then you will be able to update the state when you've component re-rendering.
Use the following way to update the state.
useEffect(() => {
setListNames(dataMatch.slice(0, 10));
}, [dataMatch]);
For more information on useEffect hook, please refer https://reactjs.org/docs/hooks-effect.html
Hope this will solve your issue.
You haven't set the state for 'matchUrl'.Do the following modification to your code.
function TableList({ match }) {
let [matchUrl, setMatchUrl] = useState(match.params.name);
return (<><h1>{matchUrl}</h1></>);
}
Also when you change 'matchUrl' manually for any reason, you should use setMatchUrl('value') function to update the value
Here is the sample code. Hope this will help you 😇
import React, {useState, useEffect} from 'react'
function TableList({ match }) {
let matchUrl = match.params.name
useEffect(() => {
// YOUR_CODE_HERE WILL RUN WHEN the match.params.name is change
}, [match.params.name])
}
you can take a look also about documentation about useEffect

React and useState(): multiple re-renders if initial state is a string

So I have a functional component that displays an iframe. The src-property for the iframe is set in useState().
Here is the code:
import React, { useEffect, useState } from "react";
export default function ComponentWithIframe() {
const [iframeUrl, setIframeUrl] = useState(undefined);
useEffect(() => {
// some axios-requests
... setIframeUrl(responseFromAxios);
});
return (
<div className='Iframe'>
<iframe
id="my_iframe"
src={iframeUrl}
width="100%" height='800px'
title="I am an Iframe"/>
</div>
);
}
Now here's the strange thing: If useState() is initialized with a string, the component does multiple re-renders on mount (3 times). For example:
const [iframeUrl, setIframeUrl] = useState("There is no url defined");
But when I initialize useState() with undefined, then the component is rendered only once and everything works fine.
I found this solution more by accident and don't understand why it is working like this. Research didn't bring any explanation. Neither the official React Docs nor in-depth explanation sites like this or this. Even here on StackOverflow, I couldn't find anything.
Does anyone know what causes this behavior?
I think the re-render occurs because there is no dependency array.
It should more like this,
useEffect(() => {
// some axios-requests
... setIframeUrl(responseFromAxios);
}, []) // <- This Array
Refer: https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

useState vs. useEffect - setting initial value

I have two basic questions about React:
In order to assign an initial empty value to a piece of state (let's say player) in a functional component, are the following two approaches the same or are they different in some way?
Approach A:
const [player, setPlayer] = useState({})
Approach B:
useEffect(() => {
setPlayer({})
}, []);
When I browse to a player, the page is eventually updated with values coming from the global store so I need to avoid showing the values of the previous player while the current one is still loading. What is the best way to go about this?
Please let me know if you need more background information in order to help.
Thank you in advance!
When assigning initial value directly in useState(initialData) you are making that state available in the very first render.
On the other hand, if you are setting the initial value in a useEffect hook, it will be set on mount, but after the very first render.
useEffect(() => {
setPlayer(initialData)
}, [])
If the state variable is important also for the first render, then you should use the first approach. It is also shorter to write and makes more sense.
Example:
if you open your devtools when running this example, the debugger will pause inside the useEffect and you will see nothing will be rendered ("mount" phase), which might not be desirable. If you were to edit the code and write useState(initialData), you would see the first render did include the correct state when the (when debugger pauses code execution).
const {useState, useEffect} = React
const App = ({initialData}) => {
const [value, setValue] = useState('🔴')
useEffect(() => {
debugger; // at this point, the component has already been rendered
setValue(initialData)
}, [])
return value
}
ReactDOM.render(<App initialData={'🟢'} />, root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.10.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.10.0/umd/react-dom.production.min.js"></script>
<div id='root'></div>

Resources