66

I have an idea that this may be because I am doing some styling things to change my radio button, but I am not sure. I am setting an onClick event that is calling my function twice. I have removed it to make sure it wasn't being triggered somewhere else and the onClick seems to be the culprit.

<div
  className="CheckboxContainer"
  onClick={() =>
    this.changeShipping({ [k]: i })
  }
>
  <label>
    <div className="ShippingName">
      {shipOption.carrier
        ? shipOption.carrier.serviceType
        : null}{' '}
      {shipOption.name}
    </div>
    <div className="ShippingPrice">
      ${shipOption.amount}
    </div>
    <input
      type="radio"
      value={i}
      className="ShippingInput"
      onChange={() =>
        this.setState({
          shippingOption: {
            ...this.state.shippingOption,
            [k]: i
          }
        })
      }
      checked={
        this.state.shippingOption[k] === i
          ? true
          : false
      }
    />
    <span className="Checkbox" />
  </label>
</div>

my function is just for now a simple console log of the shipping option:

changeShipping(shipOption){
 console.log('clicked') // happening twice 
}

If there isn't any reason you see here why this would happen I can post the rest of the code, but there is a lot and I don't think it would pertain to this, but I think this is a good starting place.

Full code:

import React, { Component } from 'react'
import fetch from 'isomorphic-fetch'
import { Subscribe } from 'statable'
import { FoldingCube } from 'better-react-spinkit'

import styles from './styles'
import { cost, cartState, userInfo, itemState, Api } from '../../state'
import { removeCookies, resetCart } from '../../../injectState'

export default class ShippingOptions extends Component {
  constructor(props) {
    super(props)
    this.state = {
      shippingOption: {}
    }

    this.changeShipping = this.changeShipping.bind(this)
  }

  async changeShipping(shipOption) {
    const shipKey = Object.keys(shipOption)[0]
    // if (userInfo.state.preOrderInfo.setShip[shipKey] === shipOption[shipKey]) {
    //   return
    // }
    let updatedShipOption = {}
    Object.keys(shipOption).forEach(k => {
      updatedShipOption = userInfo.state.preOrderInfo.setShip
        ? { ...userInfo.state.preOrderInfo.setShip, [k]: shipOption[k] }
        : shipOption
    })

    userInfo.setState({
      preOrderInfo: {
        ...userInfo.state.preOrderInfo,
        setShip: updatedShipOption
      }
    })

    // Make request to change shipping option
    const { preOrderInfo } = userInfo.state

    const shippingRes = await fetch(Api.state.api, {
      body: JSON.stringify(preOrderInfo),
      method: 'POST'
    })
      .then(res => res.json())
      .catch(err => {
        let error = ''
        if (
          err.request &&
          (err.request.status === 404 || err.request.status === 502)
        ) {
          error = `Error with API: ${err.response.statusText}`
        } else if (err.request && err.request.status === 0 && !err.response) {
          error =
            'Something went wrong with the request, no response was given.'
        } else {
          error = err.response || JSON.stringify(err) || err
        }
        cartState.setState({
          apiErrors: [error],
          loading: false
        })
      })
    console.log(shippingRes)
  }

  async componentDidMount() {
    if (cartState.state.tab === 2) {
      const { shipping } = userInfo.state
      const { items, coupon } = itemState.state
      let updated = { ...shipping }
      const names = updated.shippingFullName.split(' ')
      updated.shippingFirst = names[0]
      updated.shippingLast = names[1]
      delete updated.shippingFullName
      updated.site = cartState.state.site
      updated.products = items
      updated.couponCode = coupon
      updated.addressSame = userInfo.state.addressSame
      cartState.setState({
        loading: true
      })
      const shippingRes = await fetch(Api.state.api, {
        body: JSON.stringify(updated),
        method: 'POST'
      })
        .then(res => res.json())
        .catch(err => {
          let error = ''
          if (
            err.request &&
            (err.request.status === 404 || err.request.status === 502)
          ) {
            error = `Error with API: ${err.response.statusText}`
          } else if (err.request && err.request.status === 0 && !err.response) {
            error =
              'Something went wrong with the request, no response was given.'
          } else {
            error = err.response || JSON.stringify(err) || err
          }
          cartState.setState({
            apiErrors: [error],
            loading: false
          })
        })
      console.log(shippingRes)
      return
      shippingRes.products.forEach(product => {
        const regexp = new RegExp(product.id, 'gi')
        const updatedItem = items.find(({ id }) => regexp.test(id))

        if (!updatedItem) {
          console.warn('Item not found and being removed from the array')
          const index = itemState.state.items.indexOf(updatedItem)
          const updated = [...itemState.state.items]
          updated.splice(index, 1)
          itemState.setState({
            items: updated
          })
          return
        }
        updatedItem.price = product.price
        itemState.setState({
          items: itemState.state.items.map(
            item => (item.id === product.id ? updatedItem : item)
          )
        })
      })
      updated.shippingOptions = shippingRes.shippingOptions
      Object.keys(updated.shippingOptions).forEach(k => {
        this.setState({
          shippingOption: { ...this.state.shippingOption, [k]: 0 }
        })
        updated.setShip = updated.setShip
          ? { ...updated.setShip, [k]: 0 }
          : { [k]: 0 }
      })

      updated.success = shippingRes.success
      updated.cartId = shippingRes.cartId
      updated.locations = shippingRes.locations
      userInfo.setState({
        preOrderInfo: updated
      })
      cost.setState({
        tax: shippingRes.tax,
        shipping: shippingRes.shipping,
        shippingOptions:
          Object.keys(updated.shippingOptions).length > 0
            ? updated.shippingOptions
            : null
      })
      cartState.setState({
        loading: false,
        apiErrors: shippingRes.errors.length > 0 ? shippingRes.errors : null
      })
      if (shippingRes.errors.length > 0) {
        removeCookies()
        shippingRes.errors.forEach(err => {
          if (err.includes('CRT-1-00013')) {
            itemState.setState({ coupon: '' })
          }
        })
      }
    }
  }

  render() {
    return (
      <Subscribe to={[cartState, cost, itemState]}>
        {(cart, cost, itemState) => {
          if (cart.loading) {
            return (
              <div className="Loading">
                <div className="Loader">
                  <FoldingCube size={50} color="rgb(0, 207, 255)" />
                </div>
              </div>
            )
          }
          if (cart.apiErrors) {
            return (
              <div className="ShippingErrors">
                <div className="ErrorsTitle">
                  Please Contact Customer Support
                </div>
                <div className="ErrorsContact">
                  (contact information for customer support)
                </div>
                <div className="Msgs">
                  {cart.apiErrors.map((error, i) => {
                    return (
                      <div key={i} className="Err">
                        {error}
                      </div>
                    )
                  })}
                </div>
                <style jsx>{styles}</style>
              </div>
            )
          }
          return (
            <div className="ShippingOptionsContainer">
              <div className="ShippingOptions">
                {cost.shippingOptions ? (
                  <div className="ShipOptionLine">
                    {Object.keys(cost.shippingOptions).map((k, i) => {
                      const shipOptions = cost.shippingOptions[k]
                      const updatedProducts =
                        shipOptions.products.length === 0
                          ? []
                          : shipOptions.products.map(product =>
                              itemState.items.find(
                                item => item.id === product.id
                              )
                            )
                      return (
                        <div className="ShippingInputs" key={i}>
                          {shipOptions.options.map((shipOption, i) => {
                            return (
                              <div className="ShippingSection" key={i}>
                                <div className="SectionTitle">
                                  4. {shipOption.name} Shipping Options
                                </div>
                                {updatedProducts.length > 0 ? (
                                  <div className="ShippingProducts">
                                    {updatedProducts.map((product, i) => (
                                      <div key={i}>
                                        for{' '}
                                        {shipOption.name === 'Freight'
                                          ? 'Large'
                                          : 'Small'}{' '}
                                        {product.name} from{' '}
                                        {k.charAt(0).toUpperCase() + k.slice(1)}
                                      </div>
                                    ))}
                                  </div>
                                ) : null}
                                <div
                                  className="CheckboxContainer"
                                  onClick={() =>
                                    this.changeShipping({ [k]: i })
                                  }
                                >
                                  <label>
                                    <div className="ShippingName">
                                      {shipOption.carrier
                                        ? shipOption.carrier.serviceType
                                        : null}{' '}
                                      {shipOption.name}
                                    </div>
                                    <div className="ShippingPrice">
                                      ${shipOption.amount}
                                    </div>
                                    <input
                                      type="radio"
                                      value={i}
                                      className="ShippingInput"
                                      onChange={() =>
                                        this.setState({
                                          shippingOption: {
                                            ...this.state.shippingOption,
                                            [k]: i
                                          }
                                        })
                                      }
                                      checked={
                                        this.state.shippingOption[k] === i
                                          ? true
                                          : false
                                      }
                                    />
                                    <span className="Checkbox" />
                                  </label>
                                </div>
                              </div>
                            )
                          })}
                        </div>
                      )
                    })}
                  </div>
                ) : null}
              </div>
              <style jsx>{styles}</style>
            </div>
          )
        }}
      </Subscribe>
    )
  }
}
Taylor Austin
  • 5,407
  • 15
  • 58
  • 103
  • can you please add more "surrounding" code / context. I can't tell waht is going wrong yet... – lipp Jun 12 '18 at 14:08
  • Can you try changing your method `changeShipping` to be arrow function style like `changeShipping = shipOption => {console.log('testing');}` – Isaac Jun 12 '18 at 14:09
  • When you click on a label element, the browser has to artificially create a click event on the input element also so that the state will toggle. There are plenty of duplicates to this question, I'm just looking for the right one. – 4castle Jun 12 '18 at 14:12
  • @lipp i posted all the code and Isaac I dont think you can do that type of function, you will get Unexpected token, and 4castle please post once you find – Taylor Austin Jun 12 '18 at 14:14
  • @TaylorAustin but the "changeShipping" function assigned is the dummy (console.log)? – lipp Jun 12 '18 at 14:17
  • the problem is you are calling that function on div surrounding the radio but where you are setting state again so they are getting called twice – aravind_reddy Jun 12 '18 at 14:17
  • This also has something to do with strict mode with React18, it's by choice for some of their life cycle methods, I don't know which ones – Leon Gaban Jul 24 '23 at 07:24

5 Answers5

136

Its because your app component is a wrap in StrictMode.

<React.StrictMode>
  <App />
</React.StrictMode>,

If you are using create-react-app then it is found in index.js

It is expected that setState updaters will run twice in strict mode in development. This helps ensure the code doesn't rely on them running a single time (which wouldn't be the case if an async render was aborted and later restarted). If your setState updaters are pure functions (as they should be) then this shouldn't affect the logic of your application.

https://github.com/facebook/react/issues/12856#issuecomment-390206425

AlexMelw
  • 2,406
  • 26
  • 35
Nisharg Shah
  • 16,638
  • 10
  • 62
  • 73
  • Thanks. Nobody else mentioned this! It also then took me a while to figure out that the way to disable StrictMode (https://reactjs.org/docs/strict-mode.html) is not to delete `.StrictMode` from those tags but to delete the entire wrapping `` tag. – Ryan Apr 14 '20 at 03:28
  • 5
    Ahhh https://reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects says "Strict mode can’t automatically detect side effects for you, but it can help you spot them by making them a little more deterministic. This is done by intentionally double-invoking the following functions..." – Ryan Apr 14 '20 at 03:30
  • when I changed to it breaks the code – Deepak Garg May 31 '20 at 03:03
  • 3
    No Completely remove the and your problem will solved – Nisharg Shah May 31 '20 at 10:33
  • 2
    this is what I was looking for! it worked form me. Thanks – Dory Daniel Jul 03 '20 at 17:23
  • 2
    thanks alot, this helps, i was getting error from last three days – Mohammad Yunus Aug 16 '20 at 06:49
  • 3
    I don't know how I would have found this without this answer. Thanks so much! – Tom Nov 12 '20 at 07:39
  • 1
    This comment has been very useful to me as well, thankyou!!!! – Walker Dec 03 '20 at 10:07
  • R Native means React Native?? – Nisharg Shah Jan 05 '21 at 12:07
  • 1
    I'm not using setState() at all but removing strict mode keeps my fetch() from running twice. – favor Feb 26 '22 at 00:40
54

The problem is html related, rather than React related. By default clicking a label will also trigger the onClick event of the input element it is associated with. In your case the onClick event is attached to both the label and the input. So by clicking on the label, the event is fired twice: once for the label and once for the input associated with it.

Edit: Attaching the onClick listener to the input is a possible solution to the problem

Halcyon
  • 611
  • 7
  • 7
  • 1
    Yeap just saw this thanks to @4castle, could you add that putting the onClick on the input will fix this issue and ill accept the answer. – Taylor Austin Jun 12 '18 at 14:17
  • Great, thank you! This solution (removing dropdown outside – Łukasz Jun 17 '21 at 08:39
24

Prevent calling twice by using e.preventDefault().

changeShipping(e){
   e.preventDefault();
   console.log('clicked');
}
Amruth
  • 5,792
  • 2
  • 28
  • 41
7

e.stopPropagation() is also worth exploring. I was handling an onMouseDown event, and preventDefault was not preventing multiple calls.

https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation

Robbie
  • 445
  • 5
  • 13
0

In my case, it needs both to avoid redundant calls

<React.StrictMode>
  <App />
</React.StrictMode>

from Nisharg Shah

e.preventDefault();

from Amruth

and one more thing for functional component where useEffect() method is called twice for me is bcoz, there were only one useEffect used with holds all methods to bind in FC (functional component) and dependency list. so after the change it looks like as follows:

useEffect(() => {
    console.log("AFTER CHANGE : ", data) // move to below method
    handleSubmit.bind(this);
    handleCancel.bind(this);
    testChange.bind(this);
}, [
    data // move to below method
]);

useEffect(() => {
    console.log("AFTER CHANGE : ", data)
}, [data]);
  1. we should have without dependency list for bindings
  2. To view the changed data after onChange(), then we should have useEffect with dependencyList of the data we are looking for.

Hope this helps. Happy coding . . . .

CdVr
  • 323
  • 3
  • 15