skip to Main Content

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


  1. Chosen as BEST ANSWER

    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.

    setImageList([`main/${mainPicture}`, ...(data.map(obj => `sub/${obj.name}`))])
    

    becomes -->

    setImageList([`main/${location.state?.paramPicture || mainPicture}`, ...(data.map(obj => `sub/${obj.name}`))])
    

    and I passed my location from my MainComponent to my SubComponent.


  2. 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 a useState hook.

    That means when MainComponent mounts, whatever this value was at that time is captured and copied into mainPicture. When you go to a new collection, via the link with the new paramPicture in the links state prop, the MainComponent component is only rerendered and not torn down. This means that mainPicture 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:

    import { useLocation } from 'react-router-dom';
    

    Using this hook is like reading from location.state?.paramPicture, apart from the react component will automatically rerender when that history state changes.

    Change:

    const [ mainPicture, setMainPicture ] = useState(location.state?.paramPicture || null)
    

    To

    const { state: { mainPicture = null } } = useLocation()
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search