Taylor Austin
Taylor Austin

Reputation: 6027

Why is my function being called twice in React?

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>
    )
  }
}

Upvotes: 79

Views: 115512

Answers (7)

Gavin Simpson
Gavin Simpson

Reputation: 11

For me it was also the React.strictmode but since I was using Next.JS i had to change it in the Next Config

Next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
}

module.exports = nextConfig

Upvotes: 1

Jhames Palileo
Jhames Palileo

Reputation: 21

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

Commenting </React.StrictMode> works for me.

Upvotes: 2

CdVr
CdVr

Reputation: 313

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 . . . .

Upvotes: 0

Robbie
Robbie

Reputation: 445

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

Upvotes: 7

Amruth
Amruth

Reputation: 5912

Prevent calling twice by using e.preventDefault().

changeShipping(e){
   e.preventDefault();
   console.log('clicked');
}

Upvotes: 26

Nisharg Shah
Nisharg Shah

Reputation: 19662

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

Upvotes: 158

Halcyon
Halcyon

Reputation: 691

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

Upvotes: 62

Related Questions