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; }
原因分析
- 函数组件无法直接被ref捕获:默认情况下React函数组件没有实例,父组件的ref无法直接获取到子组件的方法,必须显式暴露。
- state更新方式错误:
increaseAttempts中直接修改gameStats.attempts++属于直接变更state对象,违反React的state不可变原则,会导致state未正确更新,间接引发组件渲染异常。 - 闭包依赖问题:
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
相关产品推荐
相关产品推荐

