Home > OS >  React local state showing empty inside a function
React local state showing empty inside a function

Time:06-12

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.

  • Related