Hellodan
Hellodan

Reputation: 1208

Updating state inside child and using updated value in parent

I am trying to create a really basic header mini cart with cart item counter in NextJS. Im using the form state value in the header component and then using that value in the child components of the header where the numerical value is rendered.

Currently I have the total item count show correctly when I submit my form and I can see the correct value of items showing in the header counter and the mini cart item as expected.

However, when I click to remove the item in the mini cart I would like to reset the entire counter state back to 0 from whatever its previous value was and re-render the header and mini cart/cart item components so that the product item is hidden in the mini cart again.

Here is a basic run through of current components.

product-form.js

import React, { useState, useRef, useEffect } from "react";

export default function ProductForm(props) {
  const [count, setCount] = useState(0);

  const numberInput = useRef(null);

  useEffect(() => {
    numberInput.current.dispatchEvent(
      new Event("change", {
          detail: {
              newValue: count,
          },
          bubbles: true,
          cancelable: true,
      })
    );
  }, [count]);

  render(
    <>
     <div>
        <button onClick={() => {setCount(count - 1)}}>>
         Decrease
        </button>
       <Input 
        value={count}
        ref={numberInput}
        onChange={(e) => setNumberValue(e.target.value)}>
        <button onClick={() => {setCount(count + 1)}}>>
         Increase
        </button>
     </div>
     <button onClick={event => props.onClick(() => event.currentTarget.parentNode.parentNode.querySelector('#product-qty-input').value)}>Add to Cart</button>
    </>
  )
}

header.js

import React, { useState } from "react";
import MiniCart from './minicart'

export default function HeaderRight({count}) {
  const [cartItemCount, setCartItemCount] = useState([]);
  return (
    <>
     <span>You have {count} items </span>
     <MiniCart count={count} />
    </>
  )
}

mini-cart.js

import CartItem from "./cartitem";
import CartEmpty from "./cartEmpty";

export default function MiniCart({count}) {
  return (
    <>
     { count == 0 && 
       <CartEmpty />
     }
     { count > 0 &&
        <CartItem count={count} />
     }
    </>
  )
}

mini-cart-item.js

export default function CartItem({count}) {
  return (
    <>
      <p>Product Name</p>
      <p>Unit Price: $125.00</p>
      <p>x{count}</p>
      <p>$375.00</p>
      <button>REMOVE ITEM</button>
    </>
  )
}

UPDATE 07/09/2022:

I have fixed this issue. Issue was resolved by passing setCartCountState as a value into all of the nested children components. This then allowed me to create a click event in the mini-cart-item.js component that would allow me to use the following function to reset the counter back to 0 and hide the mini cart items.

// Reset 'countState' to '0' so that item is removed when remove item is clicked in mini cart.
  const changeState = () => {
    countState("0");
}

Answer can be demoed here: https://stackblitz.com/edit/nextjs-lzqu46?file=components/cartitem.js

Upvotes: 1

Views: 632

Answers (2)

Andy
Andy

Reputation: 63524

  1. Based on your code let say we pass in an object to the product page which has the shape

    { sku: string, name: string, count: 0 }
    

    and we add that to state.

    (For convenience I've added two objects in an array to best show off the example.)

  2. When a button is clicked the count value of that item in state is updated.

  3. When the state changes the component is re-rendered - we pass in a count of total items to the minicart which it can display. Accordingly the count of the item changes as well.

const { Fragment, useEffect, useState } = React;

// Returns child elements
function HeaderRight({ children }) {      
  return (
    <header>
      {children}
    </header>
  );
}

// Pass in the cart state, and the count
// and display either an empty cart or a count
function MiniCart({ count }) {
  if (!count) return <div class="cart">🛒 empty</div>;
  return <div class="cart">🛒&nbsp;{count} items</div>;
}

function App({ data }) {

  // Initialise the cart state using the data
  // we pass in through the component props
  const [ cart, setCart ] = useState(data);

  // A function that returns the total number of items
  // in the cart
  function getCount(cart) {
    return cart.reduce((acc, c) => acc += c.count, 0);
  }

  // Handles the count when a "decrease" button is clicked
  // and `count` is not zero decrease the count
  function handleDec(sku) {
    const copy = [...cart];
    const index = copy.findIndex(item => item.sku === sku);
    if (copy[index].count > 0) --copy[index].count;
    setCart(copy);
  }

  // Handles the count when an "increase" button is clicked
  // `count` is increased
  function handleInc(sku) {
    const copy = [...cart];
    const index = copy.findIndex(item => item.sku === sku);
    ++copy[index].count;
    setCart(copy);
  }

  // Pase in the count to the minicart, and `map`
  // over the items with the `Item` component.
  return (
    <main>
      <HeaderRight>
        <MiniCart count={getCount(cart)} />
      </HeaderRight>
      {data.map(item => {
        return (
          <Item
            item={item}
            handleDec={handleDec}
            handleInc={handleInc}
          />
        );
      })}
    </main>
  )
}

function Item({ item, handleDec, handleInc }) {
  return (
    <div className="item">
      <div className="name">{item.name}&nbsp;{item.count}</div>
      <div className="buttons">
        <button
          onClick={() => handleDec(item.sku)}
        >[-]
        </button>
        <button
          onClick={() => handleInc(item.sku)}
        >[+]
        </button>
      </div>
    </div>
  );
}

const data = [
  { sku: 1, name: 'Carrot', pricePerKg: 0.3, count: 0 },
  { sku: 2, name: 'Tractor', pricePerKg: 1003, count: 0 }
];

ReactDOM.render(
  <App data={data} />,
  document.getElementById('react')
);
.cart { margin-bottom: 1em; }
.item { border: 1px solid #dfdfdf; padding: 0.2em; margin-bottom: 0.5em; }
.buttons button { padding: 0.25em; margin-right: 0.5em; }
.buttons button:hover { cursor: pointer; }
.name { margin-bottom: 0.2em; font-size: 1.2em; color: red; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>

Upvotes: 1

Fernando Souza
Fernando Souza

Reputation: 844

Where does the count of HeaderRight come from? All the rendering logic comes from this value. If this is a state, pass down also its set function to CartItem. Then you can do the following with the button:

<button onClick={setCount(count - 1)}>REMOVE ITEM</button>

The CartItem component would look like:

export default function CartItem({count, setCount}) {
  return (
    <>
      <p>Product Name</p>
      <p>Unit Price: $125.00</p>
      <p>x{count}</p>
      <p>$375.00</p>
      <button onClick={() => setCount(count - 1)}>REMOVE ITEM</button>
    </>
  )
}

Upvotes: 1

Related Questions