Gustavo Straube
Gustavo Straube

Reputation: 3871

How to get React/recompose component updated when props are changed?

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.

Upvotes: 4

Views: 2268

Answers (2)

rmiguelrivero
rmiguelrivero

Reputation: 976

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.

Upvotes: 0

Gustavo Straube
Gustavo Straube

Reputation: 3871

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.

Upvotes: 0

Related Questions