skip to Main Content

I’m working on a project in react where I just don’t understand why selectedRow is always null. Why doesn’t the setSelectedRow function set it inside of the onClick callback? I tried logging it to the console and it always returns undefined. I’ve tried using let myVar as well and it does the same thing. I feel like there’s some secret sauce here I’m missing. Anyone else have an idea?

I expect selectedRow to be undefined on the first click, but on subsequent clicks it should be populated. Another conundrum is the fact that setSelectedGameData appears to be working just fine. I’ve validated that event.currentTarget is a defined html component

import React, {MouseEvent, useCallback, useState} from 'react';
import './App.css';
import {scheduleGridColumnDefs, sequenceGenerate} from "./util";
import ScheduleGridBodyRowCol from "./ScheduleGridBodyRowCol";


function ScheduleGridBodyRow(
  props: {
    game: any,
    setSelectedGameData: Function,
    selectedGameData: any,
    weekData: any[]
  }
) {
  const {game, setSelectedGameData, selectedGameData, weekData} = props;
  const [selectedRow, setSelectedRow] = useState<any>();
  const onClick = useCallback((event: MouseEvent<HTMLTableRowElement>) => {
    const gameId = Number(event.currentTarget.getAttribute('data-game-id'));
    console.log(selectedRow);
    if (selectedRow) {
      selectedRow.style.setProperty('--bs-table-bg', '#212529');
    } else {
      setSelectedRow(event.currentTarget);
      console.log(event.currentTarget);
      event.currentTarget.style.setProperty('--bs-table-bg', '#4d5154');
      const newlySelectedGameData = weekData.filter((game: any) => game.id === gameId)[0];
      setSelectedGameData(newlySelectedGameData);
    }
  }, [selectedRow, setSelectedGameData, weekData, setSelectedRow]);
  return (
    <tr id={game.id} data-game-id={game.id} onClick={onClick}>
      {scheduleGridColumnDefs.map((columnDef: any) => (
        <ScheduleGridBodyRowCol key={sequenceGenerate()} game={game} columnDef={columnDef}/>
        )
      )}
    </tr>
  );
}

export default React.memo(ScheduleGridBodyRow);

The specific line I can’t wrap my head around is why this line isn’t working as I’d expect it to set the selected row variable:

setSelectedRow(event.currentTarget);

Can any gracious person help out?

Edit:
I tried the solution from and I’m seeing the same issue when my code looks like:


function ScheduleGridBodyRow(
  props: {
    game: any,
    setSelectedGameData: Function,
    selectedGameData: any,
    weekData: any[]
  }
) {
  const {game, setSelectedGameData, selectedGameData, weekData} = props;
  const [selectedRow, setSelectedRow] = useState<any>();
  const onClick = useCallback((event: MouseEvent<HTMLTableRowElement>) => {
    const gameId = Number(event.currentTarget.getAttribute('data-game-id'));
    setSelectedRow((prevSelectedRow: any) => {
      console.log(prevSelectedRow);
      if (prevSelectedRow) {
        prevSelectedRow.style.setProperty('--bs-table-bg', '#212529');
        return null;
      } else {
        event.currentTarget.style.setProperty('--bs-table-bg', '#4d5154');
        return event.currentTarget;
      }
    });
      const newlySelectedGameData = weekData.filter((game: any) => game.id === gameId)[0];
      setSelectedGameData(newlySelectedGameData);
    }, [setSelectedGameData, weekData, setSelectedRow]);
  return (
    <tr id={game.id} data-game-id={game.id} onClick={onClick}>
      {scheduleGridColumnDefs.map((columnDef: any) => (
        <ScheduleGridBodyRowCol key={sequenceGenerate()} game={game} columnDef={columnDef}/>
        )
      )}
    </tr>
  );
}

in this case everytime I log prevSelectedRow it’s always undefined.

EDIT:
I’m onto it. There’s a circurlar reference with reading and updating selectedRow in the same function

2

Answers


  1. The behavior you’re observing is due to the closure nature of the useCallback hook. When you define the onClick callback with useCallback, it captures the value of selectedRow at the time of its creation. This means that even if setSelectedRow updates the state, the onClick callback will still reference the old value of selectedRow.

    To fix this, you can use a functional update with useState. This allows you to access the latest state value without relying on the closure:

    Update the setSelectedRow call to use a functional update:

    setSelectedRow(prevSelectedRow => {
       if (prevSelectedRow) {
         prevSelectedRow.style.setProperty('--bs-table-bg', '#212529');
         return null;
       } else {
         event.currentTarget.style.setProperty('--bs-table-bg', '#4d5154');
         return event.currentTarget;
       }
    });
    

    Remove the dependency selectedRow from the useCallback dependency array:

    const onClick = useCallback((event: MouseEvent<HTMLTableRowElement>) => {
      const gameId = Number(event.currentTarget.getAttribute('data-game-id'));
      setSelectedRow(prevSelectedRow => {
        if (prevSelectedRow) {
          prevSelectedRow.style.setProperty('--bs-table-bg', '#212529');
          return null;
        } else {
          event.currentTarget.style.setProperty('--bs-table-bg', '#4d5154');
          return event.currentTarget;
        }
      });
      const newlySelectedGameData = weekData.filter((game: any) => game.id === gameId)[0];
      setSelectedGameData(newlySelectedGameData);
     }, [setSelectedGameData, weekData]);
    

    By using a functional update, you can access the latest state value and make decisions based on it. This approach avoids the closure issue and ensures that you’re always working with the most recent state value.

    Login or Signup to reply.
  2. The issue you’re experiencing is due to how React handles state updates. When you call setSelectedRow, it schedules an update to the selectedRow state variable, but it doesn’t immediately update it. This is why you’re seeing undefined when you log selectedRow immediately after calling setSelectedRow.

    In your onClick function, you’re trying to access the style property of selectedRow, which is undefined at the time of the first click. This is why you’re not seeing the expected behavior.

    For this, you could use a ref to store the selected row instead of using state. A ref provides a way to access DOM nodes or react elements directly, and it persists for the full lifetime of the component.

    Example;

    import React, { useRef, useCallback } from 'react';
    
    function ScheduleGridBodyRow(
     props: {
       game: any,
       setSelectedGameData: Function,
       selectedGameData: any,
       weekData: any[]
     }
    ) {
     const {game, setSelectedGameData, selectedGameData, weekData} = props;
     const selectedRowRef = useRef<HTMLTableRowElement>(null);
    
     const onClick = useCallback((event: MouseEvent<HTMLTableRowElement>) => {
       const gameId = Number(event.currentTarget.getAttribute('data-game-id'));
       if (selectedRowRef.current) {
         selectedRowRef.current.style.setProperty('--bs-table-bg', '#212529');
       }
       selectedRowRef.current = event.currentTarget;
       selectedRowRef.current.style.setProperty('--bs-table-bg', '#4d5154');
       const newlySelectedGameData = weekData.filter((game: any) => game.id === gameId)[0];
       setSelectedGameData(newlySelectedGameData);
     }, [setSelectedGameData, weekData]);
    
     return (
       <tr id={game.id} data-game-id={game.id} onClick={onClick}>
         {scheduleGridColumnDefs.map((columnDef: any) => (
           <ScheduleGridBodyRowCol key={sequenceGenerate()} game={game} columnDef={columnDef}/>
           )
         )}
       </tr>
     );
    }
    
    export default React.memo(ScheduleGridBodyRow);
    
    

    In the above code, selectedRowRef is a ref that stores the currently selected row. When a row is clicked, the onClick function checks if selectedRowRef.current is defined, and if it is, it changes the background color of the previously selected row. Then it updates selectedRowRef.current to the newly selected row and changes its background color.

    This approach avoids the issue of trying to update and access the state variable at the same time, which was causing the problem in your original code.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search