My code:
App
const App = () => {
const [list, setList] = useState([]);
const [isLoading, setIsLoading] = useState(false);
async function fetchData() {
setIsLoading(true);
fetch("https://jsonplaceholder.typicode.com/users")
.then((response) => response.json())
.then((json) => setList(json));
setIsLoading(false);
}
useEffect(() => {}, []);
const columns = useMemo(
() => [
{
Header: "Name",
accessor: "name"
},
{
Header: "Email",
accessor: "email"
},
{
Header: "Action",
accessor: "action",
Cell: ({ cell }) => (
<button
className="btn btn-link ms-2"
title="Delete"
onClick={() => handleDelete(cell.row.original.id)}
>
Delete
</button>
)
}
],
[]
);
const data = useMemo(() => [...list], [list]);
const handleDelete = async (id) => {
console.log("list", list);
const originalList = [...list];
const newList = originalList.filter((r) => r.id !== id);
setList(newList);
//try delete action to api
};
return (
<AppTable
columns={columns}
data={data}
pageCount="3"
fetchDataFn={fetchData}
/>
);
};
AppTable
const AppTable = ({
columns,
data,
pageCount: controlledPageCount,
fetchDataFn
}) => {
const tableInstance = useTable(
{
columns,
data,
manualSortBy: true,
manualPagination: true,
pageCount: controlledPageCount,
manualGlobalFilter: true,
autoResetSortBy: false,
autoResetExpanded: false,
autoResetPage: false,
autoResetFilters: false
},
useGlobalFilter,
useSortBy,
usePagination
);
const {
getTableProps,
getTableBodyProps,
headerGroups,
rows,
prepareRow,
page,
canPreviousPage,
canNextPage,
pageCount,
gotoPage,
nextPage,
previousPage,
setPageSize,
state: { pageIndex, pageSize, sortBy, globalFilter },
preGlobalFilteredRows,
setGlobalFilter
} = tableInstance;
useEffect(() => {
fetchDataFn();
}, []);
return (
<div className="table-responsive">
<table
className="table table-row-bordered table-hover gy-5 gx-5"
{...getTableProps()}
>
<thead>
{
// Loop over the header rows
headerGroups.map((headerGroup) => (
// Apply the header row props
<tr
className="fw-bolder fs-5 text-gray-800"
{...headerGroup.getHeaderGroupProps()}
>
{
// Loop over the headers in each row
headerGroup.headers.map((column) => (
// Apply the header cell props
<th
{...column.getHeaderProps(column.getSortByToggleProps())}
>
<span
className={
column.isSorted
? column.isSortedDesc
? "table-sort-desc"
: "table-sort-asc"
: ""
}
>
{
// Render the header
column.render("Header")
}
</span>
</th>
))
}
</tr>
))
}
</thead>
{/* Apply the table body props */}
<tbody {...getTableBodyProps()}>
{
// Loop over the table rows
rows.map((row) => {
// Prepare the row for display
prepareRow(row);
return (
// Apply the row props
<tr {...row.getRowProps()}>
{
// Loop over the rows cells
row.cells.map((cell) => {
// Apply the cell props
return (
<td {...cell.getCellProps()}>
{
// Render the cell contents
cell.render("Cell")
}
</td>
);
})
}
</tr>
);
})
}
</tbody>
</table>
</div>
);
};
Here, when ever, I click on delete button I am trying to get the original list, and filter the deleted row from the list and update it. But, unfortunately, current list is showing empty inside
const handleDelete = async (id) => {
console.log("list", list); //showing empty
const originalList = [...list];
const newList = originalList.filter((r) => r.id !== id);
setList(newList);
//try delete action to api
};
which is causing the table to be empty. Here, you can test it
CodePudding user response:
add the list in useMemo dependency array ,
const columns = useMemo(
() => [
{
Header: "Name",
accessor: "name"
},
{
Header: "Email",
accessor: "email"
},
{
Header: "Action",
accessor: "action",
Cell: ({ cell }) => (
<button
className="btn btn-link ms-2"
title="Delete"
onClick={() => handleDelete(cell.row.original.id)}
>
Delete
</button>
)
}
],
[list]
);
CodePudding user response:
In the delete function, your new list is dependent on the "previous" list. Therefore, you need to call setList with a callback like so:
const handleDelete = async (id) => {
setList((prevList) =>
{
console.log("list", prevList);
const newList = prevList.filter((r) => r.id !== id);
return newList;
});
//try delete action to api
};
Replace that into your test code and notice that the previous list logs out correctly and that the deletion works right.
CodePudding user response:
I thought I might add an answer to cover why you're actually seeing an empty list:
Your problem is to do with closurs and how you are memoizing your JSX that tries to use handleDelete
:
onClick={() => handleDelete(cell.row.original.id)}
Here, your code is referring to the first handleDelete
function that gets created. By first I mean the initial handleDelete
function that gets created on the initial mount/render/execution of your App
component. When this function is defined, list
at that point in time does not contain any values, so the list
value that handleDelete accesses within its body is an empty array. Every time you re-render, your handleDelete
function is recreated, and the function body for handleDelete
has access to the new list
that was created, but your code never uses these recreated versions of handleDelete
due to your useMemo()
call for the columns
array. As you currently have this memoizing your array on the initial render/mount, the handleDelete
function within that memoized function will refer to the handleDelete
function created on the initial render - that being the one that only knows about an empty list
.
To fix this, you can use a solution such as Swiffy's answer, which is fine if you just want to update list
(which you seem to want to do), but be aware that you will still have your closure issue outside of the state-setter function. If you were trying to simply just read the list
value or some other state value within handleDelete
, then you would need a different approach (which I've shown below). We can instead do something similar to Faizal Hussain's answer, so that the object is re-computed each time the list changes. However, rather than passing list
, which isn't directly a dependency of your useMemo()
array/object, I would instead address what the linter hints at doing in your sandbox:
React Hook useMemo has a missing dependency: 'handleDelete'
You can add handleDelete
as a dependency to your useMemo()
:
const columns = useMemo(..., [handleDelete]);
this way, you can clearly see that columns
is dependent on handleDelete
changing. At the moment, a new handleDelete
is recreated each render, which would lead to a new columns
object each rerender. To address this, we can use useCallback()
to memoize the handleDelete
function and have it only recreate when list
changes (this would sit above the useMemo()
for columns
:
const handleDelete = useCallback((id) => {
const newList = list.filter((r) => r.id !== id);
setList(newList);
}, [list]);
If you change setList()
to use a state setter function setList(list => ...);
then we can remove the list
dependency. But with this approach, we can now see the correct values of our state values by accessing them directly from within the function itself and passing them as dependencies.