Alan
Alan

Reputation: 349

toggle effect with react

I'm trying to create a switcher with react that when I click it it change the price of another component and when I click again over it return the original price.

My first approach was this:

  1. I crated the input with the type of a checkbox so when checked is true change the price and when is false return the original price, some kind of toggle all handled with the funcion handleDiscount

     <input
       checked={toggle}
       onChange={handleDiscount}
       type="checkbox"
       className="switch-input"
     />
    
  2. I created the handleDiscount function that change the toggle from his initial state which is false to true and and after that a ternary operator that check the condition to set the price.

     const handleDiscount = () => {
       setToggle(!toggle);
       toggle === true ? setPrice(10) : setPrice(20);
     };
    

the problem is that when is click over the checkbox again the price don't change.

I have to work with the useEffect hook? which it's the best approach for this kind of work?

for example I wrote the same code with my knowledge in vanilaJS and work, here is the example:

const switcher = document.querySelector("input");

switcher.addEventListener("click", () => {
  const priceBasic = document.querySelector(".priceBasic");
  const pricePro = document.querySelector(".pricePro");
  const priceMaster = document.querySelector(".priceMaster");
  if (switcher.checked == true) {
    priceBasic.innerHTML = `<h1>$49.99</h1>`;
    pricePro.innerHTML = "$69.99";
    priceMaster.innerHTML = "$89.99";
  } else {
    priceBasic.innerHTML = "$19.99";
    pricePro.innerHTML = "$24.99";
    priceMaster.innerHTML = "$39.99";
  }
});

Upvotes: 2

Views: 857

Answers (4)

emeraldsanto
emeraldsanto

Reputation: 4741

@codemonkey's answer is correct but if you want something a little simpler:

function YourComponent() {
  const [price, setPrice] = useState(20);
  const [toggle, setToggle] = useState(false);

  const handleDiscount = () => {
    const newValue = !toggle;

    setToggle(newValue);
    setPrice(newValue ? 10 : 20);
  }

  // ...
}

Or if price is only computed based on toggle:

function YourComponent() {
  const [toggle, setToggle] = useState(false);

  const price = useMemo(
    () => toggle ? 10 : 20,
    [toggle]
  );

  const handleDiscount = () => {
    setToggle(!toggle);
  }

  // ...
}

Upvotes: 0

pilchard
pilchard

Reputation: 12909

While your specific question of getting the toggle to work has been answered, your example in vanilla javascript hasn't been addressed.

Ideally you wouldn't be hard coding different price options directly into the component, but would be serving an array of products which each held their own specific details including pricing options.

There are various ways of handling this, but in the snippet below your toggle has become a pointer stored in state in individual Product components that can be used to access the appropriate price property of the product object passed to that component. eg.

let product = {
  id: 1,
  name: 'Product One',
  defaultPricePoint: 'red',
  pricePoints: {
    red: "$49.99",
    blue: "$69.99",
  }
};

const [pricePoint, setPricePoint] = useState('red'); 

let price = product.pricePoints[pricePoint];
// "$49.99"

The change handler simply sets the pricePoint state using the value of the input that changed (which were mapped from the keys of the object's prices to begin with). And a useEffect is implemented to set the initial radio selection based on a default set in the product object.

The result is two fairly simple components and decent separation between data and logic.

(note the use of key values that are unique for each mapped element, and not index values)

const App = ({ products }) => {

  return (
    <div>
      {products.map(product =>
        <Product key={product.id} product={product} />
      )}
    </div>
  )
}

const Product = ({ product }) => {
  const [pricePoint, setPricePoint] = React.useState();

  React.useEffect(() => {
    setPricePoint(product.defaultPricePoint)
  }, [product]);

  const handlePricePoint = (e) => {
    setPricePoint(e.target.value);
  }

  return (
    <div>
      <h3>{product.name}</h3>
      <p>{product.pricePoints[pricePoint]}</p>
      <form>
        {Object.keys(product.pricePoints).map(p =>
          <div key={product.id + p}>
            <input
              type="radio"
              name="price-point"
              value={p}
              onChange={handlePricePoint}
              checked={pricePoint === p}
            />
            <label for={p}>{p}</label>
          </div>
        )}
      </form>
    </div>
  );
}

ReactDOM.render(
  <App products={products} />,
  document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

<script>
  const products = [
    {
      id: 1,
      name: 'Product One',
      defaultPricePoint: 'red',
      pricePoints: {
        red: "$49.99",
        blue: "$69.99",
      }
    },
    {
      id: 2,
      name: 'Product Two',
      defaultPricePoint: 'pro',
      pricePoints: {
        basic: "$19.99",
        pro: "$24.99",
        master: "$39.99",
      }
    }
  ]
</script>

<div id="root"></div>

Upvotes: 0

Drew Reese
Drew Reese

Reputation: 202667

Issue

The main issue here is that state updates are asynchronous, the toggle value isn't the updated value you just enqueued.

const handleDiscount = () => {
  setToggle(!toggle);
  toggle === true ? setPrice(10) : setPrice(20);
};

Solution

Use an useEffect to toggle the price state when the toggle state updates.

useEffect(() => {
  setPrice(toggle ? 10 : 20);
}, [toggle]);

...

const handleDiscount = () => {
  setToggle(toggle => !toggle);
};

Alternatively you can make the checkbox uncontrolled and directly set the price state when the checkbox value updates

const handleDiscount = (e) => {
  const { checked } = e.target;
  setPrice(checked ? 10 : 20);
};

...

<input
  onChange={handleDiscount}
  type="checkbox"
  className="switch-input"
/>

Upvotes: 1

codemonkey
codemonkey

Reputation: 7905

I am assuming toggle is a state variable and setToggle is s state function, in which case, this won't work:

  const handleDiscount = () => {
    setToggle(!toggle);
    toggle === true ? setPrice(10) : setPrice(20);
  };

As setToggle is asynchronous. So toggle does not actually toggle by the time it reaches toggle === true ? setPrice(10) : setPrice(20);

You should instead use useEffect and listen to the change of toggle like so:

useEffect(() => {
   toggle === true ? setPrice(10) : setPrice(20);
},[toggle])

And get rid of toggle === true ? setPrice(10) : setPrice(20); inside handleDiscount altogether.

Upvotes: 0

Related Questions