Below I have a SubComponent and a MainComponent. This is meant to be a page that displays an image collection. In the Subcomponent using the onclick() event you can swap between pictures in that collection. The MainComponent will also display links to other collections, but these are links to the same component itself.
The problem is that when I click the link to go to a new collection, my SubComponent will often use the outdated mainImage to set the imageList state. It will fetch the correct response (data) however and also currentImage will still use the correct version of mainPicture and be set correctly. So in other words only the first image of the "image chooser" will use the outdated mainImage and be set incorrectly.
SubComponent.js
import React, { useState, useEffect } from 'react'
import my_client
import stockimage
const Subcomponent = ({ mainPicture, my_url, identifier }) => {
const [ imagesList, setImageList ] = useState([])
const baseImageUrl = `${my_url}`
const [ currentImage, setCurrentImage ] = useState(`main/${mainPicture}`)
useEffect(() => {
const fetchList = async () => {
const { data, error } = await my_client
.fetch(`${identifier}`)
if (data) {
setImageList([`main/${mainPicture}`, ...(data.map(obj => `sub/${obj.name}`))])
}
if (error) {
console.log(error)
setImageList([])
}
}
fetchList()
console.log("fetched")
setCurrentImage(`main/${mainPicture}`)
}, [identifier, mainPicture])
return (
<div>
{mainPicture ? <img src={`${baseImageUrl}/${currentImage}`} /> : <img src={stockimage} />}
{ imagesList && imagesList.length > 1 &&
<div>
{ imagesList.map((item, item_index) => (
<div key={item_index}>
<img src={`${baseImageUrl}/${item}`} onClick={() => setCurrentImage(item)}/>
</div>
))}
</div>
}
</div>
)
}
export default SubComponent
MainComponent.js (simplified)
import React, { useState, useEffect, useContext } from 'react'
import SubComponent from './SubComponent'
import Context from './Context'
import my_client
import { useLocation } from 'react-router-dom'
import fetchLinks from './Functions.js'
const MainComponent = () => {
const location = useLocation()
const [ mainPicture, setMainPicture ] =
useState(location.state?.paramPicture || null)
const { identifier } = useParams()
const my_url = useContext(Context)
const [ links, setLinks ] = useState(null)
useEffect(() => {
const my_fetch = async () => {
const { data, error } = await my_client
.fetch(`${identifier}/main`)
if (data) {
setMainImage(data.mainImage)
setLinks(data.otherImagesData)
}
if (error) {
console.log(error)
}
}
if (location.state.paramPicture) {
setMainPicture(location.state.paramPicture)
fetchLinks(identifier, setLinks)
} else {
my_fetch()
}
}, [identifier, location.state.paramPicture])
return (
<div>
<SubComponent mainPicture={mainPicture} my_url={my_url} identifier={identifier} />
{links.map(item => (<Link to={`/view/${item.link}`} state={{ paramPicture: item.picture }}>))}
</div>
)
}
export default MainComponent
App.js (router)
return (
<div className="App">
<Switch>
<Route path='/view' element={<View />} />
<Route path='/view/:link' element={<MainComponent />} />
</Switch>
</div>
)
There is essentially a lot of state duplication. The key issue is most likely that you are copying the history state item
paramPictureinside local component state by passing it as the initial value of auseStatehook.That means when
MainComponentmounts, whatever this value was at that time is captured and copied intomainPicture. When you go to a new collection, via the link with the newparamPicturein the linksstateprop, theMainComponentcomponent is only rerendered and not torn down. This means thatmainPicturewill retain the old value.However, storing this in a
useStateis not needed in the first place. There is no point since the value is freely available from the history state. Copying data into local component state (useState) is a common source of bugs since now you are unnecessarily having to manage code to keep it in sync (which, regardless, is missing here anyway).That said you can't just reference
location.state?.paramPicturedirectly all over the code, because it is part of the base web API, and React wouldn't know that it changed so it also would not know that it needs to re-render.However
useLocation, does know this, because it internally hooks into page nav events.First import
useLocation:Using this hook is like reading from
location.state?.paramPicture, apart from the react component will automatically rerender when that history state changes.Change:
To