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

React实现切换板块后函数仅执行一次的方案求助

解决Pokemon页面切换板块时重复调用获取地点函数的问题

开发Pokemon网页应用时,详情页包含基础信息、招式查看、捕捉地点三个板块。点击「捕捉地点」按钮会触发获取地点的函数,但切换到其他板块再返回该板块时,这个函数会重复执行。尝试过memo、useCallback、useMemo均未解决问题,相关代码如下:

父组件代码

import { useCallback, useMemo, useState } from 'react';
import { useParams, Navigate } from 'react-router-dom';
import { usePokemon } from '../../../hooks/usePokemon';
import { useSelector } from 'react-redux';
import { PokemonAside } from './components/PokemonAside';
import { PokemonBasic } from './components/PokemonBasic';
import '../../styles/PokemonSelected.css';
import { PokemonMoves } from './components/PokemonMoves';
import { PokemonPlaces } from './components/PokemonPlaces';

export const PokemonSelected = () => {
    const { activePokemon, isLoadingPokemons } = useSelector(state => state.pokemon)
    const { startSearchingPokemon } = usePokemon();
    const { name } = useParams();
    const [ view, setView ] = useState({section: 1});

    useMemo(() => {
        startSearchingPokemon(name);
    }, [name]);

    if (activePokemon == false) {
        return <Navigate to='/'/>;
    };

    const clickPlaces = useCallback(() => {
        setView({section: 3})
    }, [name]);

    return (
        <div className='container'>
            {
                (isLoadingPokemons == true)
                ? <div>cargando...</div>
                : (
                    <div className='row'>
                        <PokemonAside pokemon={ activePokemon }/>
                        <div className="col-xl-8 col-lg-8">
                            <div className="pokemon-information">
                                <h3>Information</h3>
                                <hr />
                                <div className="pokemon-sections-information">
                                    <button type="button" className="btn btn-primary" onClick={() => setView({section: 1})}>Basic</button>
                                    <button type="button" className="btn btn-danger" onClick={() => setView({section: 2})}>Moves</button>
                                    <button type="button" className="btn btn-info" onClick={ clickPlaces }>Where to catch it?</button>
                                </div>
                                <hr />
                                {
                                    view.section === 1 ? <PokemonBasic abilities={activePokemon.abilities} stats={activePokemon.stats}/>
                                    : view.section === 2 ? <PokemonMoves moves={activePokemon.moves}/>
                                    : view.section === 3 ? <PokemonPlaces id={activePokemon.id}/>
                                    : ''
                                }
                            </div>
                        </div>
                    </div>
                )
            }
        </div>
    );
};

子组件代码

import { useMemo, useCallback, useEffect, memo } from 'react';
import { usePokemon } from '../../../../hooks/usePokemon';
import { useSelector } from 'react-redux';

export const PokemonPlaces = memo(({id}) => {
    const { placesPokemon } = useSelector(state => state.pokemon)
    const { startSearchingPlaces } = usePokemon();

    useEffect(() => {
        startSearchingPlaces(id); //this function should only execute once after executed for the first time
    }, []);

    return (
        <div>Pokemon places</div>
    )
});

问题原因

当前用三元表达式切换板块时,PokemonPlaces组件会在切换到其他板块时被卸载,切回时重新挂载。组件挂载时,内部空依赖的useEffect会再次执行,导致重复调用startSearchingPlaces。memo只能阻止props不变时的重复渲染,无法解决卸载后重新挂载的问题。

解决方法

方法1:基于Redux缓存判断是否发起请求

在子组件的useEffect中,先检查Redux中是否已有当前Pokemon的地点数据,仅在无数据时发起请求:

export const PokemonPlaces = memo(({id}) => {
    const { placesPokemon } = useSelector(state => state.pokemon)
    const { startSearchingPlaces } = usePokemon();

    useEffect(() => {
        // 检查是否已有对应id的地点数据,无数据才请求
        if (!placesPokemon || placesPokemon.id !== id) {
            startSearchingPlaces(id);
        }
    }, [id, placesPokemon, startSearchingPlaces]); // 补充必要依赖,避免闭包问题

    return (
        <div>Pokemon places</div>
    )
});

方法2:保持组件挂载状态,控制显示隐藏

修改父组件的渲染逻辑,让三个板块组件始终保持挂载,通过CSS控制显示/隐藏,避免组件卸载:

// 父组件返回部分替换原有三元表达式
<div className="sections-container">
    <PokemonBasic 
        abilities={activePokemon.abilities} 
        stats={activePokemon.stats}
        style={{ display: view.section === 1 ? 'block' : 'none' }}
    />
    <PokemonMoves 
        moves={activePokemon.moves}
        style={{ display: view.section === 2 ? 'block' : 'none' }}
    />
    <PokemonPlaces 
        id={activePokemon.id}
        style={{ display: view.section === 3 ? 'block' : 'none' }}
    />
</div>

这样组件只会挂载一次,useEffect中的请求也只会执行一次。

方法3:父组件统一控制请求时机

将获取地点的逻辑移到父组件,通过状态记录是否已请求过当前Pokemon的地点数据:

// 父组件新增状态
const [hasFetchedPlaces, setHasFetchedPlaces] = useState(false);

// 切换Pokemon时重置状态
useEffect(() => {
    setHasFetchedPlaces(false);
}, [name]);

// 修改点击捕捉地点的回调
const clickPlaces = useCallback(() => {
    setView({section: 3});
    // 未请求过才发起请求
    if (!hasFetchedPlaces) {
        startSearchingPlaces(activePokemon.id);
        setHasFetchedPlaces(true);
    }
}, [name, activePokemon.id, hasFetchedPlaces]);

此时子组件仅负责渲染数据,无需处理请求逻辑。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 10:20:39