The main problem is that setting a state variable from a callback function does not seem to successfully actually change the state, so re-rendering is not triggered, and when re-rendering happens anyway, the state has not changed as expected.
It works for me as far as that the various callbacks seem to be called when (and only when) the buttons are clicked. Logging indicates that all is ok in that regard.
But when clicking the Cancel button (or the Confirm button) and calling setShowConfirmDialog(prev => false) the dialog is expected to vanish, but it remains.
Adding a useEffect simply in order to log the expected value change indicates that it is never changed. I believe that there is some issue related to closure, or a fundamental misunderstanding of some aspect of the React rendering mechanism, or how I must be accessing a view of the state that is “stale”.
I have tried to specify the involved callback functions in a variety of ways in order to try to understand why it seems like my code refers to a “stale” state variable. To no avail. The code below is my current minimal exexample that illustrates the issue.
I’ve tried with useCallback, I’ve tried setShowConfirmDialog(false), I’ve tried passing the state variable as a prop. It is clear that I have not fully understood what goes on here.
The “parent” is here in page.js
"use client"
import { useEffect, useState } from "react";
import { DeletableRow, EditableCell, ReadOnlyCell, SelectableCell } from "./Components/EditableCell";
export default function Home() {
const [usersData, setUsersData] = useState([]);
useEffect(() => {
const dbrows =
[
{ id: 11, name: "Dotty", email: "[email protected]" },
{ id: 22, name: "Adam", email: "[email protected]" }
];
setUsersData(dbrows);
}, []);
function handleDeleteUserCB(id) { console.log("Placeholder for parent CB: handleDeleteCB: id=", id) };
let rows = usersData.map((d, i) => {
let row =
<DeletableRow id={d.id}
printName={`${d.id} ${d.name} ${d.email}`}
type="User"
key={"users__" + i}
parentHandleDeleteCB={handleDeleteUserCB}>
<ReadOnlyCell value={i} />
<ReadOnlyCell value={d.id} />
<EditableCell value={d.name} prefix="user__name__" />
<EditableCell value={d.email} prefix="user__email__" />
<ReadOnlyCell value={"Read-only text"} prefix="user__email__" />
</DeletableRow >
return row;
});
console.debug("rowsToShow=", rows);
return (
<>
<h1>User list</h1>
<table >
<tbody>
{rows}
</tbody>
</table>
</>
);
}
The display:
EditableCell.js
"use client"
import React, { useCallback, useEffect, useRef, useState } from "react";
export function DeletableRow({ printName, id, children, prefix, parentHandleDeleteCB, parentHandleUpdateCB }) {
const [deleteInitiated, setDeleteInitiated] = useState(false);
console.log("(Re)Rendering <tr>", { printName, id, children, prefix, parentHandleDeleteCB, parentHandleUpdateCB });
return (
<tr key={prefix + id} >
{/* For convenience, pass some props to all children*/
React.Children.map(children, (child) => (
React.cloneElement(child, { id, parentHandleUpdateCB })
))}
<DeleteButtonCell id={id} printName={printName}
parentHandleDeleteCB={parentHandleDeleteCB}
/>
</tr>
);
}
export function EditableCell({ id, prefix, value, parentHandleUpdateCB }) {
return (
<td id={prefix + id} contentEditable={true} style={{ border: "1px solid yellow", padding: "10px" }}
title={`Click to change ${value} to another value."`}
suppressContentEditableWarning={true} >
{value}
</td>
)
}
export function ReadOnlyCell({ value }) {
return (
<td contentEditable={false} style={{ border: "1px solid grey", padding: "10px" }}
title="Read only value">
{value}
</td>
)
}
export function DeleteButtonCell({ printName, id, parentHandleDeleteCB }) {
const [showConfirmDialog, setShowConfirmDialog] = useState(undefined);
const [itemIdToDelete, setItemIdToDelete] = useState(null);
const showConfirmDialogRef = useRef(undefined);
console.log("inline showConfirmDialog=", showConfirmDialog);
console.log("inline itemIdToDelete=", itemIdToDelete);
useEffect(() => {
console.log("useEffect showConfirmDialog=", showConfirmDialog);
console.log("useEffect itemIdToDelete=", itemIdToDelete);
}, [showConfirmDialog, itemIdToDelete]);
const handleDeleteClick = useCallback((itemId) => {
setItemIdToDelete(itemId);
setShowConfirmDialog(prev => true);
}, []);
const confirmDeleteCB = useCallback((id, parentHandleDeleteCB) => {
// Call the deletion callback in the parent component
parentHandleDeleteCB(id);
console.log("After parent call, which would have deleted id=", id, " Removing dialog.")
// Deletion complete, remove the dialog.
setShowConfirmDialog(prev => false)
console.log("delete complete - dialog should close");
}, [parentHandleDeleteCB]);
const cancelDeleteCB = useCallback(() => {
console.log("setShowConfirmDialog(false)");
setShowConfirmDialog(prev => false),
console.log("delete cancelled - dialog should close");
}, []);
console.log("Rendering DeleteButtonCell", { showConfirmDialog, printName, id });
return (
<td onClick={() => handleDeleteClick(id)}
title={`Delete ${printName}?`}><button> ❌ </button>
{showConfirmDialog && <DeleteConfirmationDialog
// setShowConfirmDialog={setShowConfirmDialog}
printName={printName} id={id}
cancelCB={cancelDeleteCB}
confirmCB={() => confirmDeleteCB(itemIdToDelete, parentHandleDeleteCB)} />
}
</td>)
}
export function DeleteConfirmationDialog({ printName, id, cancelCB, confirmCB, parentHandleDeleteCB }) {
const onCancel = () => { console.log("onCancel"); cancelCB() };
const onUserX = () => { console.log("onX"); cancelCB() };
const onConfirm = () => { console.log("onConfirm"); confirmCB() };
console.log("Rendering DeleteConfirmationDialog", { printName, id, cancelCB, confirmCB });
console.log("printName=[", printName, "]");
return (
<div >
<div style={{ border: "4px solid red" }} >
<div title="Cancel - X" onClick={() => {
onUserX();
}}>X</div>
<h3 >Confirm Deletion of {printName} </h3>
<p>Are you sure you want to delete <br /><strong>{printName}</strong>?</p>
<button title="Confirm" onClick={onConfirm}>Confirm</button>
<button title="Cancel" onClick={onCancel}>Cancel</button>
</div>
</div>
)
}