主要问题是,从回调函数设置状态变量似乎并没有成功地实际更改状态,因此不会触发重新渲染,并且无论如何发生重新渲染时,状态都没有按预期更改。
它对我有用,因为当(且仅当)单击按钮时似乎会调用各种回调。日志记录表明这方面一切正常。
但是,当单击“取消”按钮(或“确认”按钮)并调用 setShowConfirmDialog(prev => false) 时,对话框预计会消失,但它仍然存在。
添加 useEffect 只是为了记录预期值更改,表明它从未更改。我认为存在一些与闭包相关的问题,或者对 React 渲染机制的某些方面存在根本性误解,或者我必须如何访问“陈旧”状态的视图。
我尝试以各种方式指定所涉及的回调函数,以便尝试理解为什么我的代码似乎引用了“陈旧”状态变量。无济于事。下面的代码是我当前说明问题的最小示例。
我尝试过 useCallback,我尝试过 setShowConfirmDialog(false),我尝试过将状态变量作为道具传递。显然我还没有完全理解这里发生的事情。
有很多具有类似内容的问题,但即使从已回答的问题中,我也无法提取这里发生的情况或如何将其更改为工作。
“父级”位于 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>
</>
);
}
可编辑单元格.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>
)
}
这看起来是一个事件冒泡问题。
您将按钮渲染为
td
(<td onClick={() => handleDeleteClick(id)}
) 的子级,其中两个元素都有 onClick
处理程序执行相反的操作(一个设置为 true,另一个设置为 false),这会导致状态更新取消,因此存在没有重新渲染。
即使您的按钮位于对话框中,它们也是表格单元格的子项,因此它们会通过它向上传播。
一种快速但肮脏的解决方案是停止按钮上的传播。例如,您的取消函数将被修改为接受这样的事件
(e) => {e.stopPropagation();...}
。
但是,我相信更好的解决方案是以替代方式呈现对话框,这样这个问题就会消失,并使其成为未来对话框的习惯。
return (
<>
<td
onClick={() => handleDeleteClick(id)}
title={`Delete ${printName}?`}><button> ❌ </button>
</td>
{showConfirmDialog && <DeleteConfirmationDialog
// setShowConfirmDialog={setShowConfirmDialog}
printName={printName} id={id}
cancelCB={cancelDeleteCB}
confirmCB={() => confirmDeleteCB(itemIdToDelete, parentHandleDeleteCB)}
/>
}
</>
)