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

React父组件调用子组件方法时useRef未定义问题排查求助

问题

在React项目中尝试通过useRef实现父组件调用子组件方法时,出现错误:Cannot read properties of null (reading 'drawInfoBar')。调用increaseAttempts()时打印infoBarRef显示为undefined,但确认执行该方法时两个子组件已渲染。

相关代码

父组件App

const App = () => {
  const [results, setResults] = useState([]);
  let [gameStats, setGameStats] = useState({wonGame: false, attempts: 0, maxAttempts: 9, message: '', currentUser: null, randomNumber: null, adminMode: {status: false, checkingUser: false}});
  const [currentResult, setCurrentResult] = useState(0);
  const buttons = {numbers: [], nav: []};
  
  // Refs of both childs
  const gameContentRef = useRef();
  const infoBarRef = useRef();

  const increaseAttempts = () => {
    gameStats.attempts++;
    infoBarRef.current.drawInfoBar(); //Error happens on this line
  }
  
  const login = useCallback(() => {
    return new Promise((resolve) => {
      Swal.fire({
        title: 'Nombre de usuario',
        icon: 'question',
        input: 'password',
        showLoaderOnConfirm: true,
        allowOutsideClick: false,
        preConfirm: (input) => {
          return fetch(`${URL_API}?alumno=${input}`).then(response => {
            return response.json();
          }).catch(() => {
            Swal.showValidationMessage('Se ha producido un error');
          }).then(async (res) => {
            const correctUser = parseInt(res['COUNT(*)']);
            if(correctUser) {
              setGameStats({...gameStats, currentUser: input});
              resolve(true);
            }
          })
        },
      })
    }) 
  }, [])

  useEffect(() => {
    const init = async() => {
      await login();
    }
    init();
  }, [login]);

  return (
    <>
      {(gameStats.currentUser || gameStats.adminMode.status) && (
        <>
          <InfoBar ref={infoBarRef} gameStats={gameStats} results={results} setResults={setResults} currentResult={currentResult} setCurrentResult={setCurrentResult} buttons={buttons} askDeleting={askDeleting}/>
          <GameContent ref={gameContentRef} gameStats={gameStats} currentResult={currentResult} setCurrentResult={setCurrentResult} buttons={buttons} increaseAttempts={increaseAttempts} postResults={postResults} resetGame={resetGame} generateNumber={generateNumber}/>
        </>
      )}
    </>
  )
}

子组件InfoBar

const InfoBar = props => {
  const canvasRef = useRef();
  let [ctx, setCtx] = useState();

  useEffect(() => {
      canvasRef.current.width = 1000;
      canvasRef.current.height = 40;
      setCtx(canvasRef.current.getContext('2d'));
    }, []);

  // This is the method I need to call from parent
  const drawInfoBar = () => {
      let text = `Intentos: ${props.gameStats.adminMode.status ? props.results[props.currentResult].intentos : props.gameStats.attempts}/${props.gameStats.maxAttempts}`;
      if(props.gameStats.adminMode.status) text += ` | ${props.gameStats.adminMode.checkingUser} | ${props.currentResult+1}/${props.results.length}`;
  
      ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
      ctx.font = '30pt Calibri';
      ctx.fillStyle = 'white';
      ctx.textAlign = 'center';
      ctx.fillText(text, 490, 30);
      if(props.gameStats.adminMode.status) setNavButtons();
  }

  return (
      <canvas onClick={e => props.gameStats.adminMode.status ? handleInfoBarClick(e) : null} ref={canvasRef} {...props}/>
  )
}

子组件GameContent(调用increaseAttempts的部分)

const GameContent = props => {
  const canvasRef = useRef();
  let [ctx, setCtx] = useState();

  useEffect(() => {
      canvasRef.current.width = 1000;
      canvasRef.current.height = 1255;
      setCtx(canvasRef.current.getContext('2d'));
    }, [])
  const handleNumbersClick = async (evt) => {
    const rect = canvasRef.current.getBoundingClientRect();
    for(let i = 0; i < props.buttons.numbers.length; i++) {
      if(ctx.isPointInPath(props.buttons.numbers[i].object, Math.round(evt.pageX - rect.left), Math.round(evt.pageY - 114))) {
        if(props.gameStats.attempts < props.gameStats.maxAttempts && !props.gameStats.wonGame) {
          props.increaseAttempts(); //It is called on this line
        }
      }
    }
  }
  return (
    <canvas onClick={e => !props.gameStats.adminMode.status ? handleNumbersClick(e) : null} ref={canvasRef} {...props}/>
  )
  export default GameContent;
}

原因分析

  1. 函数组件无法直接被ref捕获:默认情况下React函数组件没有实例,父组件的ref无法直接获取到子组件的方法,必须显式暴露。
  2. state更新方式错误:increaseAttempts中直接修改gameStats.attempts++属于直接变更state对象,违反React的state不可变原则,会导致state未正确更新,间接引发组件渲染异常。
  3. 闭包依赖问题:login函数的useCallback依赖数组为空,但内部使用了gameStats,导致它捕获的是初始state值,更新state时可能出现渲染时机异常,影响ref的赋值。

解决方案

1. 改造InfoBar组件,暴露方法给父组件

使用forwardRef和useImperativeHandle将子组件方法暴露给父组件:

import { forwardRef, useImperativeHandle, useRef, useState, useEffect } from 'react';

const InfoBar = forwardRef((props, ref) => {
  const canvasRef = useRef();
  const [ctx, setCtx] = useState();

  useEffect(() => {
    canvasRef.current.width = 1000;
    canvasRef.current.height = 40;
    setCtx(canvasRef.current.getContext('2d'));
  }, []);

  const drawInfoBar = () => {
    if (!ctx) return; // 增加空值判断,避免二次报错
    let text = `Intentos: ${props.gameStats.adminMode.status ? props.results[props.currentResult].intentos : props.gameStats.attempts}/${props.gameStats.maxAttempts}`;
    if(props.gameStats.adminMode.status) text += ` | ${props.gameStats.adminMode.checkingUser} | ${props.currentResult+1}/${props.results.length}`;

    ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
    ctx.font = '30pt Calibri';
    ctx.fillStyle = 'white';
    ctx.textAlign = 'center';
    ctx.fillText(text, 490, 30);
    if(props.gameStats.adminMode.status) props.setNavButtons(); // 假设setNavButtons通过props传入,需确保定义
  }

  // 暴露指定方法给父组件ref
  useImperativeHandle(ref, () => ({
    drawInfoBar
  }));

  return (
    <canvas onClick={e => props.gameStats.adminMode.status ? props.handleInfoBarClick(e) : null} ref={canvasRef} {...props}/>
  )
});

export default InfoBar;

2. 修复state更新逻辑

使用函数式更新获取最新state值,避免直接修改state对象:

const increaseAttempts = () => {
  // 函数式更新确保拿到最新的state
  setGameStats(prevStats => ({
    ...prevStats,
    attempts: prevStats.attempts + 1
  }));
  // 调用前判断ref是否存在
  if (infoBarRef.current) {
    infoBarRef.current.drawInfoBar();
  }
}

3. 修复login函数的闭包问题

使用函数式更新state,减少不必要的依赖:

const login = useCallback(() => {
  return new Promise((resolve) => {
    Swal.fire({
      title: 'Nombre de usuario',
      icon: 'question',
      input: 'password',
      showLoaderOnConfirm: true,
      allowOutsideClick: false,
      preConfirm: (input) => {
        return fetch(`${URL_API}?alumno=${input}`).then(response => {
          return response.json();
        }).catch(() => {
          Swal.showValidationMessage('Se ha producido un error');
        }).then(async (res) => {
          const correctUser = parseInt(res['COUNT(*)']);
          if(correctUser) {
            // 函数式更新,避免依赖外部gameStats
            setGameStats(prevStats => ({
              ...prevStats,
              currentUser: input
            }));
            resolve(true);
          }
        })
      },
    })
  }) 
}, [setGameStats]); // 仅依赖稳定的setGameStats

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 02:01:14