setState inside useEffect not working on First Render in React JS - javascript

I'm building a ReactJS Component that uses React Awesome Slider.
What I'm trying to create is a slider with a description div under it, which changes the text then I change the Slide.
Now, I found a way to make it work but I have a problem with the setState of an object, here is the code.
SLIDER:
const AutoplaySlider = withAutoplay(AwesomeSlider);
const StaticSlider = ({slider}) => {
var images = "";
var length=0;
const [current, setCurrent] = useState(0);
const [title, setTitle] = useState([]);
const [description, setDescription] = useState([]);
switch (slider) {
case 'portfolio_sviluppo_software':
images = portfolio_description.sviluppo_software;
length= portfolio_description.sviluppo_software.length;
break;
case 'portfolio_domotica':
images = portfolio_description.domotica;
length= portfolio_description.domotica.length;
break;
case 'portfolio_digital_signage':
images = portfolio_description.digital_signage;
length= portfolio_description.digital_signage.length;
break;
case 'portfolio_ricerca_e_sviluppo':
images = portfolio_description.ricerca_e_sviluppo;
length= portfolio_description.ricerca_e_sviluppo.length;
break;
}
useEffect(
() => {
setTitle(
images.map(
(slide) => (slide.title)
)
);
setDescription(
images.map(
(desc) => (desc.data)
)
);
}, [images]
);
return(
<div>
<AutoplaySlider
play={true}
cancelOnInteraction={true}
interval={0}
onTransitionStart={slide => setCurrent(slide.nextIndex)}
className="sliderHome"
>
{images.map((image, index) => {
let src = "/image/slider/portfolio/"+image.image;
//console.log(src);
return (
<div key={index} data-src={src}>
</div>
);
})}
</AutoplaySlider>
<GalleryCaption selected={current} title={title} description={description} area={slider}/>
</div>
)
};
export default StaticSlider;
DESCRIPTION GENERATOR
const GalleryCaption = ({ selected = 0, title = [], description= [], area = 0 }) => {
const formattedIndex = selected + 1;
var title = title[selected];
var data = description[selected];
return (
<div className="containerDivDescriptionPortflio">
<div className="DivDescriptionPortflio">
<p id ={"description_portfolio_"+area} className="paragDescriptionPortflio" >
<h4>{title}</h4>
<hr></hr>
{
data.map((val) => (
<div className="rowDescriptionPortfolio">
<div className="divIndexPortfolio" dangerouslySetInnerHTML={{ __html: val.index }} >
</div>
<div className="divTextPortfolio" dangerouslySetInnerHTML={{ __html: val.text }} >
</div>
</div>
))}
</p>
</div>
</div>
);
};
export default GalleryCaption;
OBJECT EXAMPLE
{
"title":"text",
"data":[
{
"index":"text",
"text": "text"
},
{
"index":"text",
"text": "text"
}
],
"image": "folder/image.jpg"
},
(This is an element of an array of this kind of object)
Now the main problem is that if inside the use effect I only call the setTitle function all works as it should, but if I use also the setDescription all just stop working. I didn't get a specific error, but I get a white screen.
ERROR THAT I GET
Warning: Each child in a list should have a unique "key" prop.
Check the render method of `PortfolioArea`. See https://reactjs.org/link/warning-keys for more information.
at div
at PortfolioArea (http://localhost:3000/static/js/bundle.js:1618:5)
at http://localhost:3000/static/js/bundle.js:4509:78
at Routes (http://localhost:3000/static/js/bundle.js:177110:5)
at Router (http://localhost:3000/static/js/bundle.js:177043:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:176523:5)
at App
at AppProvider (http://localhost:3000/static/js/bundle.js:3289:5)
Warning: Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://reactjs.org/link/unsafe-component-lifecycles for details.
* Move data fetching code or side effects to componentDidUpdate.
* If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://reactjs.org/link/derived-state
Please update the following components: AwesomeSlider
The above error occurred in the <GalleryCaption> component:
at GalleryCaption (http://localhost:3000/static/js/bundle.js:973:5)
at div
at StaticSlider (http://localhost:3000/static/js/bundle.js:3049:5)
at div
at PortfolioArea (http://localhost:3000/static/js/bundle.js:1618:5)
at http://localhost:3000/static/js/bundle.js:4510:78
at Routes (http://localhost:3000/static/js/bundle.js:177111:5)
at Router (http://localhost:3000/static/js/bundle.js:177044:15)
at BrowserRouter (http://localhost:3000/static/js/bundle.js:176524:5)
at App
at AppProvider (http://localhost:3000/static/js/bundle.js:3290:5)
Consider adding an error boundary to your tree to customize error handling behavior.
Visit https://reactjs.org/link/error-boundaries to learn more about error boundaries.`
I've tried to change the useEffect second parameters to null and also to use a unique state for every parameter, but the problem seems to be that every time I try to set a state with an object inside the useEffect, on the first render I always get a null value inside that state.
Any tips?

I think that in StaticSlider, since images and length are calculated based on slider prop, I suggest using useMemo() to calculate them per slider change, instead of reassigning the values to the var variables, which is not how it should be done in React and invites bugs to come.
const StaticSlider = ({slider}) => {
const images = useMemo(
() => {
// calculate and return images value
// with `switch`
switch (slider) {
// ...
default:
return [];
}
},
[slider]
);
const length = useMemo(
() => {
// calculate and return length value
// with `switch`
switch (slider) {
// ...
default:
return 0;
}
},
[slider]
);
Please note that your current switch block does not have a default case, you should consider returning a default case with initial values for images and length.
Also note that the initial assignment of images is images = "" which would make it a string, but inside setDescription() you are calling images.map() which is an array method, so it won't work at the initial render of the component when images is an empty string. I think this is what causes the bug.
images = [] or default: return [] for images inside switch statement should be better.
Lastly I think you can consider using a condition check inside useEffect() to only setState on title and description when images is already populated (has length).
useEffect(
() => {
if (images.length) {
setTitle(
images.map(
(slide) => (slide.title)
)
);
setDescription(
images.map(
(desc) => (desc.data)
)
);
}
},
[images]
);

Related

How to observe change in a global Set variable in useEffect inside react component function?

I have a page wherein there are Listings.
A user can check items from this list.
Whenever the user checks something it gets added to a globally declared Set(each item's unique ID is added into this set). The ID's in this set need to be accessed by a seperate Component(lets call it PROCESS_COMPONENT) which processes the particular Listings whose ID's are present in the set.
My Listings code roughly looks like:
import React from "react";
import { CheckBox, PROCESS_COMPONENT } from "./Process.jsx";
const ListItem = ({lItem}) => {
return (
<>
//name,image,info,etc.
<CheckBox lId={lItem.id}/>
</>
)
};
function Listings() {
// some declarations blah blah..
return (
<>
<PROCESS_COMPONENT /> // Its a sticky window that shows up on top of the Listings.
//..some divs and headings
dataArray.map(item => { return <ListItem lItem={item} /> }) // Generates the list also containing the checkboxes
</>
)
}
And the Checkbox and the PROCESS_COMPONENT functionality is present in a seperate file(Process.jsx).
It looks roughly like:
import React, { useEffect, useState } from "react";
let ProcessSet = new Set(); // The globally declared set.
const CheckBox = ({lID}) => {
const [isTicked, setTicked] = useState(false);
const onTick = () => setTicked(!isTicked);
useEffect( () => {
if(isTicked) {
ProcessSet.add(lID);
}
else {
ProcessSet.delete(lID);
}
console.log(ProcessSet); // Checking for changes in set.
}, [isTicked]);
return (
<div onClick={onTick}>
//some content
</div>
)
}
const PROCESS_COMPONENT = () => {
const [len, setLen] = useState(ProcessSet.size);
useEffect( () => {
setLen(ProcessSet.size);
}, [ProcessSet]); // This change is never being picked up.
return (
<div>
<h6> {len} items checked </h6>
</div>
)
}
export { CheckBox, PROCESS_COMPONENT };
The Set itself does get the correct ID values from the Checkbox. But the PROCESS_COMPONENT does not seem to be picking up the changes in the Set and len shows 0(initial size of the set).
I am pretty new to react. However any help is appreciated.
Edit:
Based on #jdkramhoft
's answer I made the set into a state variable in Listings function.
const ListItem = ({lItem,set,setPSet}) => {
//...
<CheckBox lID={lItem.id} pset={set} setPSet={setPSet} />
)
}
function Listings() {
const [processSet, setPSet] = useState(new Set());
//....
<PROCESS_COMPONENT set={processSet} />
dataArray.map(item => {
return <ListItem lItem={item} set={processSet} setPSet={setPSet} />
})
}
And corresponding changes in Process.jsx
const CheckBox = ({lID,pset,setPSet}) => {
//...
if (isTicked) {
setPSet(pset.add(lID));
}
else {
setPSet(pset.delete(lID));
}
//...
}
const PROCESS_COMPONENT = ({set}) => {
//...
setLen(set.size);
//...
}
Now whenever I click the check box I get an error:
TypeError: pset.add is not a function. (In 'pset.add(lID)', 'pset.add' is undefined)
Similar error occurs for the delete function as well.
First of all, the set should be a react state const [mySet, setMySet] = useState(new Set()); if you want react to properly re-render with detected changes. If you need the set to be available to multiple components you can pass it to them with props or use a context.
Secondly, React checks if dependencies like [ProcessSet] has been changed with something like ===. Even though the items in the set are different, no change is detected because the object is the same and there is no re-render.
Update:
The setState portion of [state, setState] = useState([]); is not intended to mutate the previous state - only to provide the next state. So to update your set you would do something like:
const [set, setSet] = useState(new Set())
const itemToAdd = ' ', itemToRemove = ' ';
setSet(prev => new Set([...prev, itemToAdd]));
setSet(prev => new Set([...prev].filter(item => item !== itemToRemove)));
As you might notice, this makes adding and removing from a set as slow as a list. So unless you need to make a lot of checks with set.has() I'd recommend using a list:
const [items, setItems] = useState([])
const itemToAdd = ' ', itemToRemove = ' ';
setItems(prev => [...prev, itemToAdd]);
setItems(prev => prev.filter(item => item !== itemToRemove));

React component not rendering despite event firing

I'm making a blog with react, next.js, and json-server. I have come as far as dynamically loading blog posts and other UI, but now when I'm trying to load the comments dynamically as well, it's not working.
The component in question is this one.
const Comments = ({ id }) => {
const [com, setCom] = useState([]);
useEffect(() => {
const getComments = async () => {
const comment = await fetchPost(id);
if (comment["comments"].length == 0) return;
const comments = [...comment["comments"]];
setCom([...comment["comments"]]);
};
getComments();
}, []);
return (
<div>
{com.map((p) => {
console.log(p.comment);
<Comment key={p.id} comment={p.comment} />;
})}
</div>
);
};
I know that the component is getting called and have the information as I'm logging it to console inside map. What I can't get my head around is why it is not rendering as it is a near carbon copy of how I render the blog-posts.
Aside from the above, I have tried the following:
checked syntax
Running <Comment/> with and without a key
putting in strings directly inside the component com.map, instead of p.comment == does not render
lifting state and useEffect up to <Post/>
Your function is not returning anything so React has nothing to render
{com.map((p) => (
<Comment key={p.id} comment={p.comment} />;
))}
The following code returns nothing
() => { const value = 1; }
The following code returns 1
() => { const value = 1; return value;}
The following code returns 1
() => 1

How do you push a UI update to a javascript frontend?

So I'm building a simple react app that fetches a bunch of images and displays them as cards.
The intention is to show an info message until all the images have loaded, then removing the notice again.
const App = () => {
const [cardInfo, setCardInfo] = useContext(CardInfoContext)
useEffect(() => {
fetchData(setCardInfo)
}, [])
useEffect(() => {
const app = document.querySelector('.app')
for(const child of app.children){
app.removeChild(child)
}
const loadingNotice = document.createElement('h1')
loadingNotice.innerHTML = "Fetching data ..."
app.appendChild(loadingNotice) //<-- this never shows up
cardInfo.forEach( info => {
const img = document.createElement('img')
img.src = info.image
app.appendChild(img)
})
app.removeChild(loadingNotice)
}, [cardInfo])
return (
<>
<div className="app">
<h1>Fetching data...</h1>
</div>
</>
)};
What instead happens is the app stays blank until all the images are loaded, then shows all the images at once -- but never the loading notice.
Can I somehow "push" the loading indicator change to the UI independent of the rest of the rendering?
Another thing I tried was
const App = () => {
const [cardInfo, setCardInfo] = useContext(CardInfoContext)
useEffect(() => {
fetchData(setCardInfo)
}, [])
useEffect(() => {
const app = document.querySelector('.app')
if(!cardInfo) return
const loadingNotice = app.querySelector(".loadingNotice")
loadingNotice.style.display = 'block' //<-- this never shows up
cardInfo.forEach( info => {
const img = document.createElement('img')
img.src = info.image
app.appendChild(img)
})
loadingNotice.style.display = 'none'
}, [cardInfo])
return (
<>
<div className="app">
<h1 className="loadingNotice">Fetching data...</h1>
</div>
</>
)}
Which would be incorrect because I do need to remove all the images, at least, but even that only displayed the loading notice for a fraction of a second, then the component goes blank until all the images can be displayed.
useEffect observes when cardInfo is changed, not when the render the comes after fired. You can use useLayoutEffect instead.
...but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
BTW, I wouldn't combine direct DOM manipulation with React to avoid issues like this (among other reasons)
Something like
const App = () => {
const [isLoadgin, setIsLoading] = useState(true)
const [cardInfo, setCardInfo] = useContext(CardInfoContext)
useEffect(() => {
fetchData(result => {
setCardInfo(result);
setIsLoading(false);
})
}, [])
return (
<>
<div className="app">
{isLoading && <h1 className="loadingNotice">Fetching data...</h1>}
{
cardInfo.map(card => <img src={card.image} />)
}
</div>
</>
)}
You need conditional rendering instead of all that DOM manipulation you are trying in the useEffect.
...
return(
<>
<div className="app">
{ !cardInfo ? <h1 className="loadingNotice">Fetching data...</h1> : <Cards info={cardInfo} /> }
</div>
</>
)
Note: I am assuming you have something like <Cards> component that displays cards details.

React component is rendering old state before getting new data

When the component first renders it gets from the switchcase (enters ALL case because its the default value) on the usePositions hook, the value for positions and I set them there, and return them to the Positions controller.
The problem comes when the selectedClient changes from ALL to TODAY (its a context value, I change it in a Sidebar component somewhere else) and before entering the switch case in TODAY value to get the positions of today, I noticed the Positions component already rendered the old state of ALL positions again! Then a second later it renders correctly the todays positions.
I noticed this because my browser on my network tab shows some calls to the server that IndividualPosition makes it means that it rendered
This is my component where it calls the usePositions hook
export const Positions = () => {
const { selectedClient } = useSelectedClientValue();
const { loading, setLoading } = useLoadingValue();
const { positions } = usePositions(selectedClient);
const clientName =
typeof selectedClient === "string" ? selectedClient : selectedClient.name;
useEffect(() => {
setLoading(false);
}, [positions]);
let positionsView = (
<ul className="positions__list">
{positions.map((position) => (
<IndividualPosition position={position} key={position.positionId} />
))}
</ul>
);
return (
<div className="positions" data-testid="positions">
{!loading ? (
<>
<div className="positions__header">
<h2 data-testid="client-name">{clientName}</h2>
</div>
{positionsView}
</>
) : (
<div className="loading-main-window">
<Spinner />
</div>
)}
</div>
);
};
This is the hook where I fetch the data
export const usePositions = selectedClient => {
const [positions, setPositions] = useState([]);
const {setLoading} = useLoadingValue()
useEffect(() => {
setLoading(true);
switch (selectedClient) {
case 'ALL':
getPositions().then(pos => {
setPositions(pos);
});
break;
case 'TODAY':
getTodayPositions().then(pos => {
setPositions(pos);
});
break;
default:
break;
}
}, [selectedClient]);
return {positions, setPositions};
};
The useEffect runs each time the selectedClient change
It looks like the component renders again before getting the todays data and thats why it shows the old state before getting the new data, but I thought that could be avoided with the loading flag
Basically:
-Positions renders, the hook fetches allPositions, its fine
-If I change in the sidebar the value of the selectedClient context value, the Positions components renders again, rendering the IndividualComponent but with the state of allPositions.
- Instead it should wait till todaysPositions fetches to show the new state (loading should do that)
I already tried having a loading local state (my loading is a context value)
Moving the loading in the useEffects on my local component instead of my hook
Setting loading to false inside the hook after fetching the data
Any ideas?

Why is the initial state being used when I try to update state

I have a react component that uses hooks for state. I have set the initial state for home to {location:null, canCharge: 'yes'}.
I then have a couple of subcomponents that call setHome() to update the pieces of the state they are responsible for.
One sets the location, and the other sets the canCharge property of the home state.
The setter for the ChargeRadioGroup works as expected, only updating the canCharge property and has no effect on the value of location.
The PlacesAutoComplete set however seems to have captured the initial state of home, and after setting a breakpoint inside, I see that it always is called with home: {location:null, canCharge:'yes'}.
I realize I could break this single state into two separate states, one for location and one for canCharge, but I'd like to understand why this is happening instead of implementing a workaround.
export default function VerticalLinearStepper() {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const [home, setHome] = useState({
location: null,
canCharge: "yes"
});
const [work, setWork] = useState({
location: null,
canCharge: "yes"
});
const steps = getSteps();
const handleNext = () => {
setActiveStep(prevActiveStep => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep(prevActiveStep => prevActiveStep - 1);
};
return (
<div className={classes.root}>
<Stepper activeStep={activeStep} orientation="vertical">
<Step>
<StepLabel>Where do you live?</StepLabel>
<StepContent>
<Box className={classes.stepContent}>
<PlacesAutocomplete
className={classes.formElement}
name={"Home"}
onPlaceSelected={location => setHome({ ...home, location })}
googleApiKey={"<API_KEY>"}
/>
<ChargeRadioGroup
className={classes.formElement}
label="Can you charge your car here?"
value={home.canCharge}
onChange={event =>
setHome({ ...home, canCharge: event.target.value })
}
/>
The code for the PlacesAutoComplete component can be seen here
I'm guessing this has something to do with the way that this component calls it's onPlaceSelected prop, but I can't figure out exactly what's going on, or how to fix it:
useEffect(() => {
if (!loaded) return;
const config = {
types,
bounds,
fields
};
if (componentRestrictions) {
config.componentRestrictions = componentRestrictions;
}
autocomplete = new window.google.maps.places.Autocomplete(
inputRef.current,
config
);
event = autocomplete.addListener("place_changed", onSelected);
return () => event && event.remove();
}, [loaded]);
const onSelected = () => {
if (onPlaceSelected && autocomplete) {
onPlaceSelected(autocomplete.getPlace());
}
};
Updating my original answer.
Instead of this:
onPlaceSelected={location => setHome({ ...home, location })}
This:
onPlaceSelected={newlocation => setHome( (prevState) => (
{ ...prevState, location:newlocation }
))}
The set state functions can take a value, and object or a function that receives the old state and returns the new state. Because setting state is sometimes asynchronous, object state with members getting set with different calls may result in captured variables overwriting new state.
More details at this link: https://medium.com/#wereHamster/beware-react-setstate-is-asynchronous-ce87ef1a9cf3

Categories