Home > Mobile >  Undefined ref when calling child method from parent
Undefined ref when calling child method from parent

Time:09-21

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.

enter image description here

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: enter image description here

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:

  1. Wrap your InfoBar with forwardRef:
const InfoBar = forwardRef((props, ref) => {...}
  1. Bind the functions in the InfoBar component to the passed ref using useImperativeHandle hook:
const InfoBar = forwardRef((props, ref) => {

  ...
  ...

  const drawInfoBar = () => {...}

  useImperativeHandle(ref, () => ({
    drawInfoBar: drawInfoBar
  }));

  ...
  ...

}
  • Related