Problem with useEffect in a Product Listing Page

On my site I have 3 categories. When I click on one of them (for example “sneaker”), I want to display only the products in the selected category. I’m currently doing it this way:

import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import baseURL from "../../utils/baseURL";
import { fecthProductsAction } from "../../redux/slices/products/productsSlice";
import LoadingComponent from "../../components/LoadingComp/LoadingComponent";
import ErrorMsg from "../../components/ErrorMsg/ErrorMsg";
import Products from "../../components/Users/Products/Products";
import { useSearchParams } from "react-router-dom";
import {
  Accordion,
  AccordionDetails,
  AccordionSummary,
  Container,
  Grid,
} from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { fetchBrandsAction } from "../../redux/slices/brands/brandsSlice";

export default function PLP() {
  const [color, setColor] = useState("");
  const [price, setPrice] = useState("");
  const [selectedBrands, setSelectedBrands] = useState([]);
  const [page, setPage] = useState(1);
  const [loadingData, setLoadingData] = useState(false);

  const [params] = useSearchParams();
  const category = params.get("category");

  const dispatch = useDispatch();

  // Carrega marcas ao carregar a página
  useEffect(() => {
    dispatch(fetchBrandsAction());
  }, [dispatch]);

  const { products, error } = useSelector((state) => state?.products);
  const { brands } = useSelector((state) => state?.brands);
  const brandsData = brands?.data?.slice(3, 9);

  // Função para buscar produtos
  const fetchProducts = () => {
    setLoadingData(true);
    const brandString = selectedBrands.join(",");
    const productUrl = `${baseURL}/products?category=${category || ""}&brand=${
      brandString || ""
    }&color=${color || ""}&price=${price || ""}&page=${page}&limit=4`;

    try {
      dispatch(fecthProductsAction({ url: productUrl }));
    } catch (error) {
      console.error("Erro na requisição de produtos:", error);
    }
    setLoadingData(false);
  };

  // Carrega produtos ao alterar filtros ou página
  useEffect(() => {
    if (category || selectedBrands.length || color || price) {
      fetchProducts();
    }
  }, [category, selectedBrands, color, price, page, dispatch]);

  // Atualiza a lista de produtos
  const [productData, setProductData] = useState([]);
  useEffect(() => {
    if (products) {
      const uniqueNewProducts =
        products.data?.filter(
          (newProduct) => !productData.some((p) => p.id === newProduct.id)
        ) || [];
      setProductData((prevData) =>
        page === 1 ? uniqueNewProducts : [...prevData, ...uniqueNewProducts]
      );
    }
  }, [products, page]);

  console.log("dados finai", productData);

  // Reinicia a página ao mudar qualquer filtro de busca
  useEffect(() => {
    setPage(1);
    setProductData([]); // Limpa a lista de produtos quando qualquer filtro muda
  }, [category, selectedBrands, color, price]);

  // Função para gerenciar o scroll infinito
  useEffect(() => {
    const handleScroll = () => {
      if (
        window.innerHeight + document.documentElement.scrollTop >=
        document.documentElement.offsetHeight - 100
      ) {
        // Incrementa a página ao atingir o final da página
        if (!loadingData) {
          setPage((prevPage) => prevPage + 1);
        }
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [loadingData]);

  return (
    <Container fixed>
      <Grid container spacing={2} direction="row" sx={{ marginY: "5rem" }}>
        {/* Filtros de Marca */}
        <Grid item xs={12} sm={12} md={3} lg={3} sx={{ height: "auto" }}>
          <Accordion defaultExpanded>
            <AccordionSummary
              expandIcon={<ExpandMoreIcon />}
              aria-controls="panel1-content"
              id="panel1-header"
            >
              Marca
            </AccordionSummary>
            <AccordionDetails>
              <div className="space-y-2">
                {brandsData?.map((brandItem) => (
                  <div key={brandItem?._id} className="flex items-center">
                    <input
                      type="checkbox"
                      className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
                      checked={selectedBrands.includes(brandItem?.name)}
                      onChange={() =>
                        setSelectedBrands((prevSelected) =>
                          prevSelected.includes(brandItem?.name)
                            ? prevSelected.filter((b) => b !== brandItem?.name)
                            : [...prevSelected, brandItem?.name]
                        )
                      }
                    />
                    <label className="ml-3 min-w-0 flex-1 text-gray-500">
                      {brandItem?.name}
                    </label>
                  </div>
                ))}
              </div>
            </AccordionDetails>
          </Accordion>
        </Grid>

        {/* Produtos */}
        <Grid
          container
          item
          xs={12}
          sm={12}
          md={9}
          lg={9}
          sx={{
            height: products ? "auto" : "100vh",
          }}
        >
          <Products products={productData} />
          {loadingData && <LoadingComponent />}
          {error && <ErrorMsg message={error?.message} />}
        </Grid>
      </Grid>
    </Container>
  );
}

That means, I get the URL param for the category with

  const category = params.get("category");

And loading the data with useEffect based on the URL inside the funcion fecthProducts(). The thing is, when I click on a category, the items are rendered continuously, and in the end I get a list of all the products from the call on page 1, and not just the items in that selected category.

Can anyone help me?

I’m using Redux, and this is the slice:

import {
  createAsyncThunk,
  createSlice,
  rejectWithValue,
} from "@reduxjs/toolkit";
import axios from "axios";
import baseURL from "../../../utils/baseURL";

//initial state
const initialState = {
  loading: false,
  error: null,
  product: {},
  products: [],
  isAdded: false,
  isUpdated: false,
  isDeleted: false,
};

//create product action
export const addProductAction = createAsyncThunk(
  "products/create",
  async (payload, { rejectWithValue, getState, dispatch }) => {
    try {
      const {
        name,
        description,
        category,
        sizes,
        brand,
        colors,
        price,
        totalQty,
        files,
      } = payload;
      //http request

      //token
      const token = getState().users?.userAuth?.userInfo?.token;
      const config = {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      };

      //formData
      const formData = new FormData();
      formData.append("name", name);
      formData.append("brand", brand);
      formData.append("category", category);
      formData.append("description", description);
      formData.append("totalQty", totalQty);
      formData.append("price", price);

      sizes?.forEach((size) => {
        formData.append("sizes", size);
      });
      colors?.forEach((color) => {
        formData.append("color", color);
      });
      files?.forEach((file) => {
        formData.append("files", file);
      });

      const { data } = await axios.post(
        `${baseURL}/products`,
        formData,
        config
      );
      return data;
    } catch (error) {
      return rejectWithValue(error?.response?.data);
    }
  }
);

//create product action
export const updateProductAction = createAsyncThunk(
  "products/update",
  async (payload, { rejectWithValue, getState, dispatch }) => {
    try {
      const {
        id,
        name,
        description,
        category,
        sizes,
        brand,
        colors,
        price,
        totalQty,
        //files,
      } = payload;
      //http request

      //token
      const token = getState().users?.userAuth?.userInfo?.token;
      const config = {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      };

      // //formData
      // const formData = new FormData();
      // formData.append("name", name);
      // formData.append("brand", brand);
      // formData.append("category", category);
      // formData.append("description", description);
      // formData.append("totalQty", totalQty);
      // formData.append("price", price);

      // sizes?.forEach((size) => {
      //   formData.append("sizes", size);
      // });
      // colors?.forEach((color) => {
      //   formData.append("color", color);
      // });
      // files?.forEach((file) => {
      //   formData.append("files", file);
      // });

      const { data } = await axios.put(
        `${baseURL}/products/update/${id}`,
        { name, description, category, sizes, brand, colors, price, totalQty },
        config
      );
      return data;
    } catch (error) {
      return rejectWithValue(error?.response?.data);
    }
  }
);

//fetch all products
export const fecthProductsAction = createAsyncThunk(
  "products/fetch-all",
  async ({ url }, { rejectWithValue }) => {
    try {
      const { data } = await axios.get(`${url}`);
      console.log("Dados retornados da API:", data); // Adicione este log

      return data;
    } catch (error) {
      console.log(error);

      return rejectWithValue(error?.response?.data);
    }
  }
);

//fetch a single product
export const fetchProductAtion = createAsyncThunk(
  "product/details",
  async (id, { rejectWithValue }) => {
    try {
      const { data } = await axios.get(`${baseURL}/products/${id}`);
      return data;
    } catch (error) {
      return rejectWithValue(error?.response?.data);
    }
  }
);

//delete a single product
export const deleteProductAtion = createAsyncThunk(
  "product/delete",
  async (id, { rejectWithValue, getState }) => {
    //token
    const token = getState().users?.userAuth?.userInfo?.token;
    const config = {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    };
    try {
      const { data } = await axios.delete(
        `${baseURL}/products/delete/${id}`,
        config
      );
      return data;
    } catch (error) {
      return rejectWithValue(error?.response?.data);
    }
  }
);

// Action para resetar o estado de isAdded
export const resetProductAdded = createAsyncThunk("products/resetAdded", () => {
  return false;
});

//products slice
const productsSlice = createSlice({
  name: "products",
  initialState,
  extraReducers: (builder) => {
    //create product
    builder.addCase(addProductAction.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(addProductAction.fulfilled, (state, action) => {
      state.loading = false;
      state.isAdded = true;
      state.product = action.payload;
    });
    builder.addCase(addProductAction.rejected, (state, action) => {
      state.error = action.payload;
      state.loading = false;
      state.product = null;
      state.isAdded = false;
    });
    // Reset isAdded
    builder.addCase(resetProductAdded.fulfilled, (state) => {
      state.isAdded = false;
    });
    //update product
    builder.addCase(updateProductAction.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(updateProductAction.fulfilled, (state, action) => {
      state.loading = false;
      state.isUpdated = true;
      state.product = action.payload;
    });
    builder.addCase(updateProductAction.rejected, (state, action) => {
      state.error = action.payload;
      state.loading = false;
      state.product = null;
      state.isUpdated = false;
    });
    //fetch all
    builder.addCase(fecthProductsAction.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(fecthProductsAction.fulfilled, (state, action) => {
      state.loading = false;
      state.products = action.payload;
    });
    builder.addCase(fecthProductsAction.rejected, (state, action) => {
      state.loading = false;
      state.products = null;
      state.error = action.payload;
    });

    //fetch single
    builder.addCase(fetchProductAtion.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(fetchProductAtion.fulfilled, (state, action) => {
      state.loading = false;
      state.product = action.payload;
    });
    builder.addCase(fetchProductAtion.rejected, (state, action) => {
      state.loading = false;
      state.product = action.payload;
      state.product = null;
    });
    //delete single
    builder.addCase(deleteProductAtion.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(deleteProductAtion.fulfilled, (state, action) => {
      state.loading = false;
      state.product = action.payload;
      state.isDeleted = true;
    });
    builder.addCase(deleteProductAtion.rejected, (state, action) => {
      state.loading = false;
      state.error = action.payload;
      state.product = null;
      state.isDeleted = false;
    });
  },
});

//generate reducer
const productsReducer = productsSlice.reducer;

export default productsReducer;

There is no problem with the API. It works fine.

The full code is here, in case you want to point out the adjustment: https://github.com/SoaresAnjos/garimpaae

I expect to recieve just the correspondent items when I click on a category