4

I'm writing this product list component and I'm struggling with states. Each product in the list is a component itself. Everything is rendering as supposed, except the component is not updated when a prop changes. I'm using recompose's withPropsOnChange() hoping it to be triggered every time the props in shouldMapOrKeys is changed. However, that never happens.

Let me show some code:

import React from 'react'
import classNames from 'classnames'
import { compose, withPropsOnChange, withHandlers } from 'recompose'
import { addToCart } from 'utils/cart'

const Product = (props) => {

  const {
    product,
    currentProducts,
    setProducts,
    addedToCart,
    addToCart,
  } = props

  const classes = classNames({
    addedToCart: addedToCart,
  })

  return (
    <div className={ classes }>
      { product.name }
      <span>$ { product.price }/yr</span>
      { addedToCart ?
        <strong>Added to cart</strong> :
        <a onClick={ addToCart }>Add to cart</a> }
    </div>
  )
}

export default compose(
  withPropsOnChange([
    'product',
    'currentProducts',
  ], (props) => {

    const {
      product,
      currentProducts,
    } = props

    return Object.assign({
      addedToCart: currentProducts.indexOf(product.id) !== -1,
    }, props)
  }),
  withHandlers({
    addToCart: ({
      product,
      setProducts,
      currentProducts,
      addedToCart,
    }) => {
      return () => {
        if (addedToCart) {
          return
        }
        addToCart(product.id).then((success) => {
          if (success) {
            currentProducts.push(product.id)
            setProducts(currentProducts)
          }
        })
      }
    },
  }),
)(Product)

I don't think it's relevant but addToCart function returns a Promise. Right now, it always resolves to true.

Another clarification: currentProducts and setProducts are respectively an attribute and a method from a class (model) that holds cart data. This is also working good, not throwing exceptions or showing unexpected behaviors.

The intended behavior here is: on adding a product to cart and after updating the currentProducts list, the addedToCart prop would change its value. I can confirm that currentProducts is being updated as expected. However, this is part of the code is not reached (I've added a breakpoint to that line):

return Object.assign({
  addedToCart: currentProducts.indexOf(product.id) !== -1,
}, props)

Since I've already used a similar structure for another component -- the main difference there is that one of the props I'm "listening" to is defined by withState() --, I'm wondering what I'm missing here. My first thought was the problem have been caused by the direct update of currentProducts, here:

currentProducts.push(product.id)

So I tried a different approach:

const products = [ product.id ].concat(currentProducts)
setProducts(products)

That didn't change anything during execution, though.

I'm considering using withState instead of withPropsOnChange. I guess that would work. But before moving that way, I wanted to know what I'm doing wrong here.

Gustavo Straube
  • 3,744
  • 6
  • 39
  • 62
  • Sounds like `currentProducts` gets mutated rather than updated. What does `setProducts` look like? – Oblosys May 18 '18 at 11:34
  • This is how it looks: `@action setProducts = (products) => { this.set('products', products) }`. Not sure if this is relevant in any wya, but this model class where `setProducts` is uses [immutable.Record](https://facebook.github.io/immutable-js/docs/#/Record) and [mobx.observable](https://mobx.js.org/refguide/observable.html) for its internal data object. – Gustavo Straube May 18 '18 at 11:42
  • @Oblosys Look at my answer below. It looks like the `currentProducts` wasn't the issue. That doesn't, in fact, answer my original question, though. – Gustavo Straube May 18 '18 at 12:48
  • I'm afraid I won't be able to help you much further, as I feel the code above should work when `currentProducts` is modified rather than mutated. You could try passing a function as the first argument to `withPropsOnChange` to investigate what happens exactly to the properties. – Oblosys May 18 '18 at 13:12

2 Answers2

0

As I imagined, using withState helped me achieving the expected behavior. This is definitely not the answer I wanted, though. I'm anyway posting it here willing to help others facing a similar issue. I still hope to find an answer explaining why my first code didn't work in spite of it was throwing no errors.

export default compose(
  withState('addedToCart', 'setAddedToCart', false),
  withHandlers({
    addToCart: ({
      product,
      setProducts,
      currentProducts,
      addedToCart,
    }) => {
      return () => {
        if (addedToCart) {
          return
        }
        addToCart(product.id).then((success) => {
          if (success) {
            currentProducts.push(product.id)
            setProducts(currentProducts)
            setAddedToCart(true)
          }
        })
      }
    },
  }),
  lifecycle({
    componentWillReceiveProps(nextProps) {
      if (this.props.currentProducts !== nextProps.currentProducts ||
          this.props.product !== nextProps.product) {
        nextProps.setAddedToCart(nextProps.currentProducts.indexOf(nextProps.product.id) !== -1)
      }
    }
  }),
)(Product)

The changes here are:

  1. Removed the withPropsOnChange, which used to handle the addedToCart "calculation";
  2. Added withState to declare and create a setter for addedToCart;
  3. Started to call the setAddedToCart(true) inside the addToCart handler when the product is successfully added to cart;
  4. Added the componentWillReceiveProps event through the recompose's lifecycle to update the addedToCart when the props change.

Some of these updates were based on this answer.

Gustavo Straube
  • 3,744
  • 6
  • 39
  • 62
0

I think the problem you are facing is due to the return value for withPropsOnChange. You just need to do:

withPropsOnChange([
    'product',
    'currentProducts',
  ], ({
      product,
      currentProducts,
    }) => ({
      addedToCart: currentProducts.indexOf(product.id) !== -1,
    })
)

As it happens with withProps, withPropsOnChange will automatically merge your returned object into props. No need of Object.assign().

Reference: https://github.com/acdlite/recompose/blob/master/docs/API.md#withpropsonchange

p.s.: I would also replace the condition to be currentProducts.includes(product.id) if you can. It's more explicit.

rmiguelrivero
  • 926
  • 8
  • 8