You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React同组件跳转时SubComponent状态异常问题排查

问题描述

我有SubComponent和MainComponent两个组件,用于展示图片集:

  • SubComponent通过onclick()事件切换当前集内图片;
  • MainComponent还会展示指向其他图片集的链接,且这些链接指向组件自身。

问题:点击链接跳转至新图片集时,SubComponent的imageList状态常使用旧的mainPicture设置,不过接口能获取正确的响应(data),currentImage也能使用正确的mainPicture设置,仅图片选择器的首张图片状态错误。

相关代码

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 (简化版)

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 (路由)

return (
<div className="App">
    <Switch>
        <Route path='/view' element={<View />} />
        <Route path='/view/:link' element={<MainComponent />} />
    </Switch>
</div>
)
问题原因

核心是异步请求的竞态条件:当切换图片集时,旧的fetchList请求还未完成,新的请求已经触发。旧请求完成后,会使用当时闭包捕获的旧mainPicture值更新imageList,覆盖新请求的正确结果。

另外,SubComponent的useEffect依赖identifier和mainPicture,每次这两个值变化都会重新执行,但异步请求的执行顺序无法保证,旧请求晚于新请求完成时就会导致状态被旧值覆盖。

解决方案

方案1:取消未完成的请求(推荐)

使用AbortController取消旧请求,避免旧结果干扰新状态:

useEffect(() => {
    const controller = new AbortController();
    const signal = controller.signal;

    const fetchList = async () => {
        try {
            const { data, error } = await my_client
              .fetch(`${identifier}`, { signal }) // 传入取消信号
            if (data) {
              setImageList([`main/${mainPicture}`, ...(data.map(obj => `sub/${obj.name}`))])
            }
            if (error) {
              console.log(error)
              setImageList([])
            }
        } catch (err) {
            if (err.name !== 'AbortError') {
                console.error('Fetch error:', err);
                setImageList([]);
            }
        }
    }

    fetchList()
    setCurrentImage(`main/${mainPicture}`)

    // 组件卸载或依赖变化时取消请求
    return () => controller.abort();
}, [identifier, mainPicture])

方案2:验证请求上下文

请求完成后,检查当前组件的identifier和mainPicture是否与请求发起时一致,不一致则不更新状态:

useEffect(() => {
    const currentIdentifier = identifier;
    const currentMainPicture = mainPicture;

    const fetchList = async () => {
        const { data, error } = await my_client
          .fetch(`${currentIdentifier}`)
        if (error) {
          console.log(error)
          setImageList([])
          return;
        }
        // 仅当上下文一致时更新状态
        if (data && identifier === currentIdentifier && mainPicture === currentMainPicture) {
          setImageList([`main/${currentMainPicture}`, ...(data.map(obj => `sub/${obj.name}`))])
        }
    }

    fetchList()
    setCurrentImage(`main/${mainPicture}`)
}, [identifier, mainPicture])

额外修复:MainComponent笔误

MainComponent中setMainImage应为setMainPicture,否则mainPicture状态无法正确更新:

// 原错误代码
if (data) {
  setMainImage(data.mainImage)
  setLinks(data.otherImagesData)
}
// 修改后
if (data) {
  setMainPicture(data.mainImage)
  setLinks(data.otherImagesData)
}

内容的提问来源于stack exchange,提问作者StackerCen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 23:35:59