Search code examples
reactjsreact-hooksag-grid-react

React - useEffect render only for first action


I am displaying a grid of products with AgGridReact. In this grid I can delete the products, when I delete one the grid update, but if I delete others the grid doesn't update again (but the products are deleted from the database, I need to reload the page to see it). I'm not sure why my useEffect only works with the first deletion.

My component :

import { useState, useEffect, useMemo } from "react";
import { Col, Row, Button } from "react-bootstrap";
import { LinkContainer } from "react-router-bootstrap";
import AdminLinksComponent from "../../../components/admin/AdminLinksComponent";
import { AgGridReact } from "ag-grid-react";
import { redirectionFromAdminPages } from "../../../utils/Redirection";

import "ag-grid-community/styles/ag-grid.css";
import "ag-grid-community/styles/ag-theme-alpine.css";
import { useDispatch } from "react-redux";

const ProductPageComponent = ({ fetchProducts, deleteProduct }) => {
  const [productDeleted, setProductDeleted] = useState(false);
  const reduxDispatch = useDispatch();

  const deleteHandler = (productId) => {
    if (
      window.confirm(
        "Voulez-vous vraiment supprimer ce produit ?\nCette action est irreversible."
      )
    ) {
      deleteProduct(productId)
        .then(data => {
          if (data === "Product deleted") {
            setProductDeleted(!productDeleted);
            console.log(productDeleted)
            window.alert("Produit supprimé !");
          } else {
            console.error(data);
          }
        })
        .catch(err => console.error(err))

    }
  };

  const editOrDeleteBtn = (data) => {
    return (
      <>
        <LinkContainer to={`/admin/modifier-produit/${data.value}`}>
          <Button className="btn-sm">
            <i className="bi bi-pencil-square"></i>
          </Button>
        </LinkContainer>
        {" / "}
        <Button
          className="btn-sm"
          variant="danger"
          onClick={() => deleteHandler(data.value)}
        >
          <i className="bi bi-x-circle"></i>
        </Button>
      </>
    );
  };

  const formatData = (data) => {
    const products = [];
    const todayDate = new Date();
    data.map((product) => {
      let productPromotion = "NON";
      if (product.promoId) {
        const startDate = new Date(product.promoStart);
        const endDate = new Date(product.promoEnd);
        if (todayDate >= startDate && todayDate <= endDate) {
          productPromotion = product.discountRate.toString() + "% (En cours)";
        } else if (todayDate < startDate) {
          productPromotion = product.discountRate.toString() + "% (A venir)";
        } else if (todayDate > endDate) {
          productPromotion = product.discountRate.toString() + "% (FINI)";
        }
      }

      let obj = {
        reference: product.ref,
        name: product.name,
        price: Number(product.price),
        stock: Number(product.stock),
        promotion: productPromotion,
        category: product.category,
        subcategory: product.subcategory,
        editOrDelete: product.id,
      };
      return products.push(obj);
    });
    return products;
  };

  const [rowData, setRowData] = useState([]);
  // eslint-disable-next-line
  const [colDefs, setColDefs] = useState([
    {
      field: "reference",
      headerName: "#",
      width: 80,
    },
    {
      field: "name",
      headerName: "Nom",
      minWidth: 100,
      maxWidth: 180,
      flex: 2,
    },
    {
      field: "price",
      headerName: "Prix",
      width: 100,
    },
    {
      field: "category",
      headerName: "Catégorie",
      minWidth: 100,
      maxWidth: 180,
      flex: 2,
    },
    {
      field: "subcategory",
      headerName: "Sous-catégorie",
      minWidth: 100,
      maxWidth: 180,
      flex: 2,
    },
    {
      field: "stock",
      headerName: "Stock",
      width: 90,
    },
    {
      field: "promotion",
      headerName: "Promotion",
      minWidth: 100,
      maxWidth: 180,
      flex: 2,
    },
    {
      field: "editOrDelete",
      headerName: "Modifier / Supprimer",
      width: 150,
      filter: false,
      cellRenderer: editOrDeleteBtn,
    },
  ]);

  const defaultColDef = useMemo(
    () => ({
      sortable: true,
      filter: true,
      resizable: true,
      editable: true,
    }),
    []
  );

  useEffect(() => {
    console.log("useEffect...")
    const abctrl = new AbortController();
    fetchProducts(abctrl)
      .then((data) => formatData(data))
      .then((res) => {
        setRowData(res)
      })
      .catch((er) => {
        const errorMessage = er.response.data.message
          ? er.response.data.message
          : er.response.data;
        redirectionFromAdminPages(errorMessage, reduxDispatch);
      });

    return () => abctrl.abort();
  }, [fetchProducts, productDeleted, reduxDispatch]);

  return (
    <Row className="my-5 mx-3">
      <Col md={2}>
        <AdminLinksComponent />
      </Col>
      <Col md={10} id="admin-products-list-container">
        <Row id="admin-products-list-title-row" className="mb-3">
          <h1 id="admin-products-list-title">Liste des produits</h1>
          <LinkContainer to="/admin/ajouter-produit">
            <Button
              variant="primary"
              size="lg"
              id="admin-products-list-create-btn"
            >
              Ajouter un nouveau produit
            </Button>
          </LinkContainer>
        </Row>
        <div id="ag-grid-products-div" className="ag-grid-divs" style={{ height: "603px" }}>
          <AgGridReact
            style={{ height: "100%" }}
            className="ag-theme-alpine"
            defaultColDef={defaultColDef}
            rowData={rowData}
            columnDefs={colDefs}
            animateRows={true}
            rowSelection={true}
            sizeColumnsToFit={true}
            pagination={true}
            paginationAutoPageSize={true}
          />
        </div>
      </Col>
    </Row>
  );
};

export default ProductPageComponent;

The parent:

import ProductPageComponent from "./components/ProductsPageComponent";

import axios from "axios";

const fetchProducts = async (abctrl) => {
  const { data } = await axios.get("/api/products/admin", {
    signal: abctrl.signal,
  });
  return data.productList;
};

const deleteProduct = async (productId) => {
  const { data } = await axios.delete(`/api/products/admin/${productId}`);
  return data;
};

const AdminProductsPage = () => {
  return (
    <ProductPageComponent
      fetchProducts={fetchProducts}
      deleteProduct={deleteProduct}
    />
  );
};

export default AdminProductsPage;

I tried using React Developper Tools (profiler) but I don't understand the results... Here is the result from a record with 2 deletions: profiler result 1 profiler result 2 profiler result 3 profiler result 4 profiler result 5

Edit: I added a console.log(productDeleted) in the deleteHandler, it gets console every time I delete a product (even when it doesn't trigger the useEffect), but its value never changes (always returns false). I also add a console.log("useEffect...") at the beginning of the useEffect hook, which shows the hook is not triggered when I delete more than one product.


Solution

  • If someone has the same probleme, here is the solution :

    In the deleteHandler function, instead of triggering the useEffect to refetch all data, I delete the row using ag-grid api + I order the server to remove data in the db. I also deleted the productDeleted state variable.

    Here is what I changed in my code:

    const deleteHandler = (rowData) => {
        if (
          window.confirm(
            "Voulez-vous vraiment supprimer ce produit ?\nCette action est irreversible."
          )
        ) {
          rowData.api?.applyTransaction({ remove: [rowData.data] });
          deleteProduct(rowData.value)
            .then(data => {
              if (data === "Product deleted") {
                window.alert("Produit supprimé !");
              } else {
                console.error(data);
              }
            })
            .catch(err => console.error(err));
        }
      };
    
      const editOrDeleteBtn = (rowData) => {
        return (
          <>
            <LinkContainer to={`/admin/modifier-produit/${rowData.value}`}>
              <Button className="btn-sm">
                <i className="bi bi-pencil-square"></i>
              </Button>
            </LinkContainer>
            {" / "}
            <Button
              className="btn-sm"
              variant="danger"
              onClick={() => deleteHandler(rowData)}
            >
              <i className="bi bi-x-circle"></i>
            </Button>
          </>
        );
      };