5

I have the following "Buy" button for a shopping cart.

I also have a component called Tooltip, which will display itself for error/success messages. It uses the button's width to determine it's centre point. Hence, I use a `ref since I need to access it's physical size within the DOM. I've read that it's bad news to use a ref attribute, but I'm not sure how else to go about doing the positioning of a child component that is based off the physical DOM. But that's another question... ;)

I am persisting the app's state in localStorage. As seen here: https://egghead.io/lessons/javascript-redux-persisting-the-state-to-the-local-storage

The issue I'm running into is that I have to clear the state's success property before rendering. Otherwise, if I have a success message in the state, on the initial render() the Tooltip will attempt to render as well. This won't be possible since the button it relies on is not yet in the DOM.

I thought that clearing the success state via Redux action in componentWillMount would clear up the success state and therefore clear up the issue, but it appears that the render() method doesn't recognize that the state has been changed and will still show the old value in console.log().

My work-around is to check if the button exists as well as the success message: showSuccessTooltip && this.addBtn

Why does render() not recognize the componentWillMount() state change?

Here is the ProductBuyBtn.js class:

import React, { Component } from 'react';
import { connect } from 'react-redux'

// Components
import Tooltip from './../utils/Tooltip'

// CSS
import './../../css/button.css'

// State
import { addToCart, clearSuccess } from './../../store/actions/cart'

class ProductBuyBtn extends Component {

 componentWillMount(){
  this.props.clearSuccess()
 }

 addToCart(){
  this.props.addToCart(process.env.REACT_APP_SITE_KEY, this.props.product.id, this.props.quantity)
 }

 render() {

  let showErrorTooltip = this.props.error !== undefined
  let showSuccessTooltip = this.props.success !== undefined

  console.log(this.props.success)

  return (
   <div className="btn_container">
    <button className="btn buy_btn" ref={(addBtn) => this.addBtn = addBtn } onClick={() => this.addToCart()}>Add</button>
    {showErrorTooltip && this.addBtn &&
     <Tooltip parent={this.addBtn} type={'dialog--error'} messageObjects={this.props.error} />
    }
    {showSuccessTooltip && this.addBtn &&
     <Tooltip parent={this.addBtn} type={'dialog--success'} messageObjects={{ success: this.props.success }} />
    }
   </div>
  );
 }
}

function mapStateToProps(state){
 return {
  inProcess: state.cart.inProcess,
  error: state.cart.error,
  success: state.cart.success
 }
}

const mapDispatchToProps = (dispatch) => {
 return {
  addToCart: (siteKey, product_id, quantity) => dispatch(addToCart(siteKey, product_id, quantity)),
  clearSuccess: () => dispatch(clearSuccess())
 }
}

export default connect(mapStateToProps, mapDispatchToProps)(ProductBuyBtn)
Gurnzbot
  • 3,742
  • 7
  • 36
  • 55

1 Answers1

4

Well, it seems to be a known problem that's easy to get into (harder to get out of, especially in a nice / non-hacky way. See this super-long thread).

The problem is that dispatching an action in componentWillMount that (eventually) changes the props going in to a component does not guarantee that the action has taken place before the first render.

So basically the render() doesn't wait for your dispatched action to take effect, it renders once (with the old props), then the action takes effect and changes the props and then the component re-renders with the new props.

So you either have to do what you already do, or use the components internal state to keep track of whether it's the first render or not, something like this comment. There are more suggestions outlined, but I can't list them all.

jonahe
  • 4,820
  • 1
  • 15
  • 19
  • 1
    Thank you. I wasn't sure if there was a 'best practice' or not. You stated my initial thoughts on the matter in that the component renders before the state is actually changed. It's weird though because I did a console.log in the reducer to see when it was firing, and it seemed to take place before the render. I guess it's deceiving. – Gurnzbot Aug 31 '17 at 20:40
  • 1
    No problem! I should say I'm no Redux expert, so I recommend at least taking a glance at the thread I linked, just to be sure I didn't misrepresent the content. But I do remember running into similar situations before (and I don't remember them ever getting a clean solution). I wouldn't be all too surprised if it turns out the issue can be avoided if the application is structured in some other way, but I don't know how (and as far as I could tell, neither did the people in the thread, or at least there was no consensus about how.) – jonahe Aug 31 '17 at 20:50
  • 1
    I quickly went over the thread you posted (thank you!). It seems that everyone is truly doing work-arounds. Maybe they aren't work arounds actually since it's just how it's being done ;) This is one of those time I'm glad everyone has this issue and it's not me being a lame dev. – Gurnzbot Sep 01 '17 at 17:44
  • Haha, I know the feeling :) – jonahe Sep 01 '17 at 17:45
  • I just drove myself insane for a few hours before I decided to just clear the entire state before i even changed the route. That fixed it, but is hacky. – Z2VvZ3Vp Nov 06 '18 at 21:47