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>
)
2
Answers
I solved this by using my router state directly, so I wouldn't need to rely on my MainComponent's
const [ mainPicture, setMainPicture ] = useState()
and don't have to update that manually, when I simply have a more relaible router state available.becomes -->
and I passed my location from my MainComponent to my SubComponent.
There is essentially a lot of state duplication. The key issue is most likely that you are copying the history state item
paramPicture
inside local component state by passing it as the initial value of auseState
hook.That means when
MainComponent
mounts, whatever this value was at that time is captured and copied intomainPicture
. When you go to a new collection, via the link with the newparamPicture
in the linksstate
prop, theMainComponent
component is only rerendered and not torn down. This means thatmainPicture
will retain the old value.However, storing this in a
useState
is 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?.paramPicture
directly 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