Send information from multiple children components to one parent component in React

Viewed 1169

I have one parent component that renders the total sum of the subtotals that are within children components.

function Total({ products }) {
  const [total, setTotal] = useState(0);

  const handleChildToParent = (subtotal) => {
    setTotal(total + subtotal);
  };

  return (
    <div>
      {products.map((product) => (
        <Subtotal product={product} childToParent={handleChildToParent} />
      ))}
      <span>Total: {total}</span>
    </div>
  );
}

Each Subtotal component takes in the price of an item, and calculates the subtotal based on how many units the user chooses.

function Subtotal({ product, childToParent }) {
  const [units, setUnits] = useState(0);

  const handleInputChange = (value) => {
    setUnits(value);
    childToParent(value * product.price);
  };

  return (
    <div>
      <input
        type="number"
        min={1}
        onChange={(e) => handleInputChange(e.target.value)}
        value={units}
      />
      <span>units * product price: {units * product.price}</span>
    </div>
  );
}

My goal is to have a dynamic total in the parent component based on the information of its children. For example: the user chooses two items with price of $100 each, and three with price $12, the parent component should display $236, and the child components should display $200 ($100x2) for the first item, and $36 ($12x3) for the second item (this is what I currently have), and that if the user changes his mind on the first item and instead of two wants to get just one, the total price should update to $136. When I try to subtract an item I'm stuck. I've tried setting an initial sum of items and using useEffect with total but I can't get to the solution. Any help will be appreciate it, I actually don't know if this is really possible using child to parent communication between components.

Here's a sandbox of what I have: https://codesandbox.io/s/reverent-elbakyan-i0hr1

2 Answers

This is a classic example of where you should "lift state up" out of your "subtotal" components and into the parent.

Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.

In this case, the place where "several components need to reflect the same changing data" is your <Total> component, because as you said:

My goal is to have a dynamic total in the parent component based on the information of its children.

One of the core patterns of React is that the "the data flows down". In other words, the data/state should live at a high level in your app and "flow down" from there to descendants. Trying to have data live inside of "child" components (<Subtotal>) and ALSO update state in its ancestors is an anti-pattern in React and should be avoided, because it means you are tracking the same state in two or more places at once.

Try something like this:

function Total({ products }) {
  // track "subtotals" in one place in state;
  // the total value of all subtotals can be trivially derived from this
  const [subtotals, setSubtotals] = React.useState(new Array(products.length).fill(0));

  // our onChange handler will update the index of "subtotals"
  // with the passed new value
  const onChange = (newValue, i) => {
    setSubtotals(oldSubtotals => {
      const newSubtotals = [...oldSubtotals];
      newSubtotals[i] = newValue;
      return newSubtotals;
    });
  }

  let total = 0;
  for (let i = 0; i < subtotals.length; i++) {
    total += subtotals[i] * products[i].price;
  }

  return (
    <div>
      {products.map((product, i) => (
        <Subtotal product={product} value={subtotals[i]} onChange={onChange} index={i} />
      ))}
      <span>Total: {total}</span>
    </div>
  );
}

// now <Subtotal> tracks no state, it is entirely stateless;
// it simply displays the correct information and calls its onChange as needed
function Subtotal({ product, value, onChange, index }) {
  return (
    <div>
      <input
        type="number"
        // updated min to 0, unless you really want to force
        // them to buy at least one...?
        min={0}
        onChange={(e) => onChange(e.target.value, index)}
        value={value}
      />
      <span>units * product price: {value * product.price}</span>
    </div>
  );
}

Now, the source of truth for what the total values is <Total> and ONLY <Total>. There is no weird splitting of state where one component is trying to mirror the state of other components. <Total> knows exactly what values each subtotal is tracking, and simply passes that value to a child component, as well as a function to update that state. But you are not tracking state in two places, which is very very bad.

Updated sandbox link

How about using array for state in Total component?

I tried not to change as much as possible from the code you wrote before.

const initialState = [0, 0];

function Total({ products }) {
  const [values, setValues] = useState(initialState);
  // Below is for sum of array
  const total = values.reduce((prev, val) => prev + val);

  return (
    <div>
      {products.map((product, idx) => {
        const handleChildToParent = (value) => {
          setValues(values => {
            const newValues = [...values];
            newValues[idx] = value;
            return newValues;
          })
        }

        return (
          <Subtotal product={product} childToParent={handleChildToParent} />
        );
      })}
      <span>Total: {total}</span>
    </div>
  );
}
Related