I need to call a child method from parent in React, therefore I am using useRef
. However, I get Cannot read properties of null (reading 'drawInfoBar')
when calling it.
Parent:
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}/>
</>
)}
</>
)
}
Child:
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}/>
)
}
increaseAttempts()
is called from GameContent
component:
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;
}
If I print infoBarRef
inside increaseAttempts()
, it is undefined:
Why is this happening, and how to fix it? Both refs should be already assigned in increaseAttempts
method, since I am running it after both components are rendered.
CodePudding user response:
Passing the ref
to the child component is not enough, you need to bind the functions in the child component to the ref
using useImperativeHandle hook:
- Wrap your
InfoBar
withforwardRef
:
const InfoBar = forwardRef((props, ref) => {...}
- Bind the functions in the
InfoBar
component to the passedref
using useImperativeHandle hook:
const InfoBar = forwardRef((props, ref) => {
...
...
const drawInfoBar = () => {...}
useImperativeHandle(ref, () => ({
drawInfoBar: drawInfoBar
}));
...
...
}