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
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:
Remove the dependency selectedRow from the useCallback dependency array:
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.
The issue you’re experiencing is due to how React handles state updates. When you call
setSelectedRow
, it schedules an update to theselectedRow
state variable, but it doesn’t immediately update it. This is why you’re seeingundefined
when you logselectedRow
immediately after callingsetSelectedRow
.In your
onClick
function, you’re trying to access thestyle
property ofselectedRow
, which isundefined
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;
In the above code,
selectedRowRef
is a ref that stores the currently selected row. When a row is clicked, theonClick
function checks ifselectedRowRef.current
is defined, and if it is, it changes the background color of the previously selected row. Then it updatesselectedRowRef.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.