Edit: I found the solution as a result of cbreezier's advice, as well as learning more about how the table hook was working (which I had found on YouTube and foolishly did not fully understand). Sending currentData
back to ParentComponent
via the props callback was what I took from their answer. However, passing currentData
to the callback was not working from tableHooks
. I then realized that while it was able to access the rows, it was not able to access the currentData. Changing Cell: ({ row })
to Cell: ({ currentData, row })
did the trick.
If you are trying to figure out a similar issue in your own React app, this may not be the exact answer, but I would suggest understanding cbreezier's answer and hopefully that sets you on your way.
Original Question:
I have been encountering an issue for a few days now that appears to be rooted in my inexperience with React and Javascript, but without knowing what the issue is, it has been difficult to search for a solution. Here is some example code that somewhat isolates the issue:
export const ParentComponent = () => {
const [currentData, setCurrentData] = useState([]);
const fetchCurrentData = async () => {
let fetchedCurrentData = await getFunctions.getCurrentData();
setCurrentData(fetchedCurrentData);
}
const handleButtonPress = (row) => {
console.log(currentData);
}
return (
<div>
<ChildComponent data={currentData} handleButtonPress={handleButtonPress} />
</div>
)
}
export default function ChildComponent(props) => {
const columns = useMemo(() => COLUMNS, []);
const data = useMemo(() => props.data, [props.data]);
const tableHooks = (hooks) => {
hooks.visibleColumns.push((columns) => [
...columns, {
id: "add",
Header: "Add",
Cell: ({ row }) => (
<button onClick={() => props.handleButtonPress()}>Add</button>
)
}
])
}
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow
} = useTable({
columns,
data
}, tableHooks);
return (
<div>
<table {...getTableProps()}>
<thead>
{headerGroups.map(headerGroup => (
<tr {...headerGroup.getHeaderGroupProps()}>
{
headerGroup.headers.map(column => (
<th {...column.getHeaderProps()}>{column.render('Header')}</th>
))
}
</tr>
))}
</thead>
<tbody {...getTableBodyProps()}>
{rows.map((row, i) => {
prepareRow(row);
return (
<tr {...row.getRowProps()}>
{
row.cells.map(cell => {
return <td {...cell.getCellProps()}>{cell.render('Cell')}</td>;
})
}
</tr>
);
})}
</tbody>
</table>
</div>
)
}
The flow is as so:
currentData
state variable is updated using setCurrentData
ParentComponent
is re-renderedChildComponent
is also rendered and receives currentData
via propsChildComponent
receives a function handleButtonPress
via propsChildComponent
uses react-table
(v7.8.0) to display the data, and then via a hook is able to create an additional column in the table, with a button for each rowhandleButtonPress
from props onClickhandleButtonPress
is calledThe above flow is working correctly. The issue occurs at the end of this flow, when the function handleButtonPress
is called from ChildComponent
, as console.log(currentData)
logs an empty array instead of the actual state of currentData
. I have verified that currentData
is populated before re-render of ParentComponent
, and that it successfully reaches ChildComponent
.
Would greatly appreciate some help with this! If I had to guess where I'm going wrong, it's that something about passing the function handleButtonPress
via props is potentially sending a version of the function before currentData
is set, but like I said I'm quite inexperienced with React and that's just a guess!
Thanks for reading.
There are two pieces of missing information that makes it impossible to answer your question with 100% certainty:
fetchCurrentData
?tableHooks
used and invoked?However, even without that information, I can give you a pretty good idea about what the issue is.
The first thing to understand is that every time ParentComponent
re-renders, you are defining a completely new handleButtonPress
function.
The second thing to understand is that the currentData
variable in ParentComponent
changes identity after setCurrentData
is called within the fetchCurrentData
function. What that means is that it's actually a completely difference variable on the second re-render of ParentComponent
compared to the first.
The third thing to understand is that your handleButtonPress
function forms a closure around the currentData
variable. However, because the identity of currentData
changes between re-renders, the variable being closed over is actually different. In other words, the handleButtonPress
function from the first re-render has closed over the currentData
variable from the first re-render, which is stuck as []
. The handleButtonPress
function from the second re-render has closed over the currentData
variable from the second re-render, which contains your actual data.
Finally, I suspect that tableHooks
is only invoked once during the first render of ChildComponent
, and that the props.handleButtonPress
function is the first handleButtonPress
function which is stuck with the first currentData
which is []
.
The solution?
Pass through the relevant data through to the handleButtonPress
function in the invocation of the function. Ie, change the function signature to:
const handleButtonPress = (row, data) => {
console.log(data);
}
Incidentally, now that function is a pure function - it has no side effects (well, other than logging to console) and depends purely on its inputs. You can move that function to be defined outside of your render loop for a (very small) performance improvement, and a pretty useful way to declare "this is indeed a pure function".
In general, prefer pure functions and avoid relying on side effects to avoid tricky situations like you encountered here.