javascriptreactjsreact-state

React unable to trigger usestate change at right time


I'm trying to sort products by their price low-high or high-low, something I've done before in vanilla JS, but this time in React. I have a dropdown to chose the desired order and the actual function works but not in the way I need it too.

In my local version of the code if you select the sort order from the dropdown it does nothing at that point, however if you then filter the products by say a brand or category it applies both the brand (or category) filter and also the selected price sort order filter. You can then from that point change the sort order from low-high to high-low or vice versa just fine.

So it does seem to apply the selected sort order correctly but it's just not rendering the useState change at the time of selection in the dropdown and I'm not sure why.

In short here's the bits controlling the sort order functionality:

const [filterData, setFilterData] = useState(productDataList.products || []);

const sortItems = (e) => {
console.log('Sort');
const sortedProducts = filterData.sort((a, b) => {
  if (e.value == "LowToHigh") {
    console.log('sorted low to high');
    if (a.price < b.price) {
      return -1;
    } else {
      return 1;
    }
  } else if (e.value == "HighToLow") {
    console.log('sorted high to low');
    if (a.price > b.price) {
        return -1;
      } else {
        return 1;
      }
    }
  });
  setFilterData(sortedProducts);
};

return (

<select id="sortDropDown" onChange={(e) => sortItems(e.target)}>
    <option value="default">Sort by:</option>
    <option value="LowToHigh">Low to high</option>
    <option value="HighToLow">High to low</option>
</select>

)

Hopefully that is enough for someone to help provide information on a fix, as I'm confused why it's not updating when selecting an option from the dropdown.

If not I've added more code below for the page (although it's a slightly stripped back version and is normally broken up across a few components and I've tried to amalgamate it below, but if anything else is needed to be able to help please let me know.

Thanks in advance.

const {useState, useEffect}  = React

const productDataList = {
  "products": [
    {
      "id": 1,
      "title": "Essence Mascara Lash Princess",
      "category": "beauty",
      "price": 9.99,
      "brand": "Essence",
      "thumbnail": "https://cdn.dummyjson.com/products/images/beauty/Essence%20Mascara%20Lash%20Princess/thumbnail.png"
    },
    {
      "id": 2,
      "title": "Eyeshadow Palette with Mirror",
      "category": "beauty",
      "price": 19.99,
      "brand": "Glamour Beauty",
      "thumbnail": "https://cdn.dummyjson.com/products/images/beauty/Eyeshadow%20Palette%20with%20Mirror/thumbnail.png"
    },
    {
      "id": 3,
      "title": "Powder Canister",
      "category": "beauty",
      "price": 14.99,
      "brand": "Velvet Touch",
      "thumbnail": "https://cdn.dummyjson.com/products/images/beauty/Powder%20Canister/thumbnail.png"
    },
    {
      "id": 4,
      "title": "Dior J'adore",
      "category": "fragrances",
      "price": 89.99,
      "brand": "Dior",
      "thumbnail": "https://cdn.dummyjson.com/products/images/fragrances/Dior%20J'adore/thumbnail.png"
    },
    {
      "id": 5,
      "title": "Dolce Shine Eau de",
      "category": "fragrances",
      "price": 69.99,
      "brand": "Dolce & Gabbana",
      "thumbnail": "https://cdn.dummyjson.com/products/images/fragrances/Dolce%20Shine%20Eau%20de/thumbnail.png"
    },
    {
      "id": 6,
      "title": "Annibale Colombo Sofa",
      "category": "furniture",
      "price": 2499.99,
      "brand": "Annibale Colombo",
      "thumbnail": "https://cdn.dummyjson.com/products/images/furniture/Annibale%20Colombo%20Sofa/thumbnail.png"
    },
  ],
  "total": 194,
  "skip": 0,
  "limit": 36
}

const App = () => {

  const [productsLimit, setProductsLimit] = useState(productDataList.limit)
  const [filterData, setFilterData] = useState(productDataList.products || []);
  const [categories, setCategories] = useState([]);
  const [brands, setBrands] = useState([]);

  useEffect(() => {
    let filteredCategories = [],
        filteredBrands = [];
        productDataList.products.forEach((product) => {
      if (!filteredCategories.includes(product.category)) {
        filteredCategories.push(product.category);
      }
      if (!filteredCategories.includes(product.brand)) {
        filteredBrands.indexOf(product.brand) == -1 && product.brand != '' && product.brand != undefined && product.brand != null ? filteredBrands.push(product.brand) : null;
      }
    });
    setCategories(filteredCategories);
    setBrands(filteredBrands);
  }, []);

  const productFilters = (products, filt) => {
    let filteredProducts = [];
    if (categories.includes(filt)) {
      filteredProducts = products.category.toLowerCase() === filt.toLowerCase();
    } else if (brands.includes(filt)) {
      filteredProducts = products.brand === filt;
    }
    return filteredProducts;
  };

  const filter = (byFilter) => {
    if (byFilter) {
      let productsData = [...productDataList.products];
      productsData = productsData.filter((filter) => {
        return productFilters(filter, byFilter);
      });
      setFilterData(productsData);
    } else {
      setFilterData(productDataList.products);
    }
  };

  const sortItems = (e) => {
    console.log('Sort');
      const sortedProducts = filterData.sort((a, b) => {
        if (e.value == "LowToHigh") {
          console.log('sorted low to high');
          if (a.price < b.price) {
            return -1;
          } else {
            return 1;
          }
        } else if (e.value == "HighToLow") {
          console.log('sorted high to low');
          if (a.price > b.price) {
              return -1;
            } else {
              return 1;
            }
        }
      });
      setFilterData(sortedProducts);
  };

  return (

    <div>
      <select id="sortDropDown" onChange={(e) => sortItems(e.target)}>
        <option value="default">Sort by:</option>
        <option value="LowToHigh">Low to high</option>
        <option value="HighToLow">High to low</option>
      </select>
      <select onChange={(e) => filter(e.target.value)}>
        <option value="Filter Categories" selected="selected">Filter by categories</option>
        {categories.map((catFilter) => (
          <option value={catFilter}>{catFilter}</option>
        ))}
      </select>
      <select onChange={(e) => filter(e.target.value)}>
        <option value="Filter Categories" selected="selected">Filter by brands</option>
        {brands.map((item) => (
          <option value={item}>{item}</option>
        ))}
      </select>
      <button onClick={filter}>Clear filter</button>
    


      <div className='products'>

        {filterData?.slice(0, productsLimit).map(product => (
          <div className='product' key={product.id}>
            <div className='productImage'>
              <img src={product.thumbnail} />
            </div>
            <div className='productInfo'>
              <div className='productTitle'>
                <div className='productBrand'>{product.brand}</div> 
                <div className='productName'>{product.title}</div>
                <div className='productName'>{product.title}</div>
              </div>
              <div className='productPrice'>Price: £{product.price}</div>
              {}
            </div>

          </div>
        ))}
      </div>
    </div>
  )
}


ReactDOM.render(<App />, document.querySelector('#root'));
.products {
  display: flex;
  flex-wrap: wrap;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

<div id="root"></div>


Solution

  • Thanks to @Robin Zigmond for his comment, just for quicker reference of an answer I'm adding it here.

    Changing .sort to .toSorted did instantly solve the issue.

    const sortItems = (e) => {
      console.log('Sort');
      const sortedProducts = filterData.toSorted((a, b) => {
        if (e.value == "LowToHigh") {
          console.log('sorted low to high');
          if (a.price < b.price) {
            return -1;
          } else {
            return 1;
          }
        } else if (e.value == "HighToLow") {
          console.log('sorted high to low');
          if (a.price > b.price) {
              return -1;
            } else {
              return 1;
            }
          }
      });
      setFilterData(sortedProducts);
    };
    

    This is explained because:

    sort mutates the array and returns the same reference (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), so React doesn't "see" any change. The simplest solution, if you don't need to support browsers older than a year or so, is to use the new toSorted https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted] method instead of sort

    I still think a solution/workaround that would work with older browsers would be beneficial and I'm happy to mark that as the answer if one is added but for now thanks to @Robin Zigmond for this solution.