4

I'm building an E-commerce app with React and I stumbled on a problem that React doesn't render the UI based on the initial state when first start the page.

Problem description:

  • I have a sort state which has the initial state of "latest", based on this Sorting functionality - if sort has a value of "latest" - it will sort and return the newest items first.
  • But on start or when I refresh the page, the default value and state of sort will still be "latest" but the UI just display the oldest items first.
  • I have to click on other option and then choose the Latest option again for the sort logic to go through. When I refresh the page, the problem is back.
  • The logic for sorting other values works fine. In the demo below, you can see I log out the sort current state. On start, the sort value is already "latest".
  • In the API product.js file, I already sorted the items with mongoose by the field createdAt - 1 but seems like it doesn't apply on the UI?

-> What would be the case here that makes React not render the items based on initial state and how can we fix it?

Below is my code:

ProductList.jsx

const ProductList = () => {
  const location = useLocation()
  const category = location.pathname.split("/")[2]
  const [filters, setFilters] = useState({});
  const [sort, setSort] = useState("latest");

  // For Filters Bar
  const handleFilters = (e) => {
    const value = e.target.value
    // When choosing default value, show all products:
    if (value === "") {
      return setFilters([])
    } else {
      setFilters({
        ...filters,
        [e.target.name]: value,
      })
    }
  }

  return (
    <Container>
      <Navbar />
      <Announcement />
      <Title>Dresses</Title>
      <FilterContainer>
        <Filter>
          <FilterText>Filter Products: </FilterText>
          <Select name="color" onChange={handleFilters}>
            <Option value="">All Color</Option>
            <Option value="white">White</Option>
            <Option value="black">Black</Option>
            <Option value="brown">Brown</Option>
            <Option value="red">Red</Option>
            <Option value="blue">Blue</Option>
            <Option value="yellow">Yellow</Option>
            <Option value="green">Green</Option>
          </Select>
          <Select name="size" onChange={handleFilters}>
            <Option value="">All Size</Option>
            <Option>XS</Option>
            <Option>S</Option>
            <Option>M</Option>
            <Option>L</Option>
            <Option>XL</Option>
            <Option>36</Option>
            <Option>37</Option>
            <Option>38</Option>
            <Option>39</Option>
            <Option>40</Option>
            <Option>41</Option>
            <Option>42</Option>
            <Option>43</Option>
          </Select>
        </Filter>
        <Filter>
          <FilterText>Sort Products: </FilterText>
          <Select onChange={e => setSort(e.target.value)}>
            <Option value="latest">Latest</Option>
            <Option value="oldest">Oldest</Option>
            <Option value="asc">Price ↑ (Low to High)</Option>
            <Option value="desc">Price ↓ (High to Low)</Option>
          </Select>
        </Filter>
      </FilterContainer>
      <Products category={category} filters={filters} sort={sort} />
      <Newsletter />
      <Footer />
    </Container>
  );
}

Products.jsx

const Products = ({ category, filters, sort }) => {

  const [products, setProducts] = useState([])
  const [filteredProducts, setFilteredProducts] = useState([])

  useEffect(() => {
    const getProducts = async () => {
      try {
        const res = await axios.get( category 
          ? `http://localhost:5000/api/products?category=${category}` 
          : `http://localhost:5000/api/products`
        )

        setProducts(res.data)
      } catch (err) {
        console.log(`Fetch all items failed - ${err}`)
      }
    }
    getProducts()
  }, [category])

  useEffect(() => {
    category && setFilteredProducts(
      products.filter(item => 
        Object.entries(filters).every(([key, value]) => 
          item[key].includes(value)
        )
      )
    )
  }, [category, filters, products])

  // Sorting:
  useEffect(() => {
    console.log(sort)
    if (sort === "latest") {
      setFilteredProducts(prev =>
        [...prev].sort((a, b) => b.createdAt.localeCompare(a.createdAt))
      )
    } else if (sort === "asc") {
      setFilteredProducts(prev =>
        [...prev].sort((a, b) => a.price - b.price)
      )
    } else if (sort === "desc") {
      setFilteredProducts(prev =>
        [...prev].sort((a, b) => b.price - a.price)
      )
    } else {
      setFilteredProducts(prev =>
        [...prev].sort((a, b) => a.createdAt.localeCompare(b.createdAt))
      )
    }
  }, [sort])

  return (
    <Container>
      <Title>Popular In Store</Title>
      <ProductsWrapper>
        {filteredProducts.map(item => (
        <Product key={item._id} item={item} />
      ))}
      </ProductsWrapper>
    </Container>
  );
}

API Route - product.js

const router = require('express').Router()
const { verifyTokenAndAdmin } = require('./verifyToken')
const Product = require('../models/Product')

// .... (Other CRUD)

// GET ALL PRODUCTS
router.get("/", async(req, res) => {
  const queryNew = req.query.new
  const queryCategory = req.query.category

  try {
    let products = []
    
    if(queryNew) {
      products = await Product.find().sort({ createdAt: -1 }).limit(5)
    } else if (queryCategory) {
      products = await Product.find({ 
        categories: {
          $in: [queryCategory],
        }, 
      })
    } else {
      products = await Product.find()
    }

    res.status(200).json(products)
  } catch(err) {
    res.status(500).json(`Cannot fetch all products - ${err}`)
  }
})

module.exports = router

Demo:

demo gif

  • Explain demo: On start, it renders oldest items first. Have to choose another option and then return to the latest option for it to render. But in the console, the initial state of sort is already "latest" but it doesn't match with the useEffect sorting logic.

Update

According @idembele70's answer, I mistyped the initial state of filters to Array.

  • I have fixed it and also added a name="sort" on the Sort select.
  • I also replaced value="latest" with defaultValue="latest" for my Sort select bar. -> This makes the Latest option stop working so I don't think it can be used in this case?
  • The result is still the same, the UI doesn't render the logic of the Sort bar to display the latest items first.

Code

const ProductList = () => {
  const location = useLocation()
  const category = location.pathname.split("/")[2]
  const [filters, setFilters] = useState({});
  const [sort, setSort] = useState("latest");

  const handleFilters = (e) => {
    const value = e.target.value
    // When choosing default value, show all products:
    if (value === "") {
      setFilters({}) // Changed from array to object
    } else {
      setFilters({
        ...filters,
        [e.target.name]: value,
      })
    }
  }

...

 <Filter>
  <FilterText>Sort Products: </FilterText>
  <Select name="sort" onChange={e => setSort(e.target.value)} >
    <Option defaultValue="latest">Latest</Option>
    <Option value="oldest">Oldest</Option>
    <Option value="asc">Price ↑ (Low to High)</Option>
    <Option value="desc">Price ↓ (High to Low)</Option>
  </Select>
</Filter>
3
  • Your sorting logic is only running whenever the sort state changes. The ideal solution would be to apply the sort logic after you get the data from your axios call but before setProducts. Then the first render would have sorted data. Alternatively you could trigger the sorting useEffect hook whenever the products state changes. Commented Apr 8, 2022 at 8:11
  • Another option, are you also in control over the API? If so, then you could send the sort state, just like you did with category, to the API and let the server provide you with pre-sorted products. Commented Apr 8, 2022 at 8:19
  • Hi @EmielZuurbier, thank you for suggesting valuable solutions. But I'm afraid I don't understand how to apply it in my code, can you help me understand how to apply the solutions you offered? Yes, I do have control of the API Commented Apr 9, 2022 at 4:04

3 Answers 3

2

You shouldn't put the changed products to the state, as it makes it extra complex to keep it updated and you need to deal with various useEffect cases. Instead it's better to define sorting and filtering functions and apply them at the render time. This way you'll ensure the render result is always up-to-date with the data:

const filterProducts = (products) => {
  if (!category) {
    return products;
  }
  return products.filter(item =>
    Object.entries(filters).every(([key, value]) =>
      item[key].includes(value),
    ),
  );
};

const sortProducts = (products) => {
  switch (sort) {
    case "asc":
      return [...products].sort((a, b) => a.price - b.price);
    case "desc":
      return [...products].sort((a, b) => b.price - a.price);
    case "latest":
    default:
      return [...products].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
  }
};

return (
  <Container>
    <Title>Popular In Store</Title>
    <ProductsWrapper>
      {sortProducts(filterProducts(products)).map(item => (
        <Product key={item._id} item={item} />
      ))}
    </ProductsWrapper>
  </Container>
);

Sign up to request clarification or add additional context in comments.

2 Comments

This works for me, thank you. Now I know there's a different approach instead of using multiple useEffect. But because of the default case, this would take effect on every where I place this Products component. I also tried the answer below and it makes sense because the .sort query logic in my api overrides the front-end's sort logic on initial load. But now if i have >1000 items, I want to know if this approach or the backend sorting would be better for performance/memory?
You could add a sorted prop to Products component and use it in sortProducts to skip the sorting. Sorting on backend could be faster, but then users would need to wait for api request to complete each time they sort the products, which is slower than doing it client-side. If you have 1000+ items you should consider adding pagination.
1
+50

If you put log inside your useEffect hooks, I would assume the execution order is:

[sort]->[category]->[category, filters, products]

The useEffect of [category] has an ajax call and it will definitely take effect after [sort], when it taking effect, it will run your query which is not using req.query.new, so it will just run this branch(i guess you do have category)

else if (queryCategory) {
    products = await Product.find({ 
        categories: {
          $in: [queryCategory],
        }, 
      })
    }

, it should default return a ascending list just match 'oldest' option.

So in general, your sort effect is not working at all on initial load because it will always get overwritten by other effects.

So either you make all the query with default descending condition to match the default 'latest' option, or you could trigger sort after any query was performed, option 2 looks better but you need to consider what's your expected behavior of sort when changing other elements(filter, category).

1 Comment

Yes, indeed I'm in a category /products/women so it runs queryCategory and returns all the items of women category but doesn't sort it. So I added .sort({ createdAt: -1 }) after the` .find({ categories: ... })` and now it works just as expected. I would appreciate if you can demonstrate your 2 options so I can understand more way to solve this. Thank you so much!
1

i faced a problem with this code from lama, so first i see a error from you define useState with a initialState as object and in your handleFilters you use an array

const [filters, setFilters] = useState({}); // you use Object there
const handleFilters = (e) => {
const value = e.target.value
products:
if (value === "") {
  return setFilters([]) //. you should use object there not array.
} else {
  setFilters({
    ...filters,
    [e.target.name]: value,
  })
}}

So in React when you use onChange it's recommended to use a value or defaultValue in your html elems to solve this problem take a look to this CodeSandebox link: https://codesandbox.io/s/determined-hopper-gp9ym9?file=/src/App.js Let me know if it has solved your problem.

1 Comment

Thank you, I have fixed the state of Array to Object and tried adding a defaultValue = "latest" instead of value = "latest" but it makes that Latest option broke. And the Sort bar still doesn't render out the sorted latest items first on start. Please check my update above. Does this have anything to do with the mongoose sort query in the API?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.