Alexander Simonsen
Alexander Simonsen

Reputation: 17

How do I set state of selected values from a two-dimensional array with a form.select?

I'm having some problems getting the value price from the useState products when I'm using a Form.Select.

The idea is that with the Form.Select you're able to select a product by its name and then onChangeProduct will set the state of data with the given product name and the price property of that product. I'm just not sure how to fetch the price value without the user needing to manually selecting a price in a Form.Select too.

The code is a simple illustration of the code I got so fare.

If you have any component that would make this easier than I'll be glad to hear about it.

Hope that makes sense.

const [data, setData] = useState([]);

const [product, setProduct] = useState([
    { name: "TV", price: 1000 },
    { name: "Phone", price: 3000 },
  ]);

const onChangeProduct = (name, value) => {
   setData((prevValue) => {
      const newValues = [...prevValues];
      newValues = Object.assign({}, newValues, {[name]: value });
      return newValues;
   });
};

return (
<Form>
   <Form.Select
   onChange={(event) => {onChangeProduct("name", event.target.value);}}
   value={product.name}
   name="product"
   >
      {product.map((item) => {
         return <option value={item.name}>{item.name}</option>;
      })}
   </Form.Select>
</Form>
);

EDIT

I've decided to go another way to make this work. I may need to set the products to an array/state as I want to be able to catch both the price and product later on.

The idea is to make the system work with add/remove product to a card where you'll be able to check the checkout with the productname and price. Therefore you're able to pick a list of products and in that way see the price property of that product.

I used the same functions I got help with here: onChange, onProductRemove and add function

CardForm.jsx -> ModalEditProducts.jsx -> ProductEdit.jsx

In CardForm.jsx I check if there's a ID and if there's a ID I'll fetch all the products already selected by this user.

CardForm.jsx

const CardForm = () => {
  const [data, setData] = useState({});

  const { id } = useParams();

  const [products, setProducts] = useState([]);

  useEffect(() => {
    if (id) {
      const fetchData = async () => {
        const docRef = doc(db, "user", id);
        try {
          const docSnap = await getDoc(docRef);
          setData(docSnap.data());
        } catch (error) {
          console.log(error);
        }
      };
      fetchData().catch(console.error);
    }
  }, []);

  const handleProductChanged = (product) => {
    setData((data) => ({ ...data, product }));
  };

  return (
    <>
      <Container className="mb-3 content_container_primary">
        <Row>
          <Col xs={12} md={12} lg={8} className="">
            <Form>
              <div className="box content_pa">
                <Col xs={12} md={12} lg={12}>
                  <div>
                    <ModalEditProducts
                      onProductChanged={handleProductChanged}
                      data={data.product ?? []}
                      title="Edit products"
                    />
                  </div>
                </Col>
              </div>
            </Form>
          </Col>
        </Row>
      </Container>
    </>
  );
};

ModalEditProducts.jsx

const ModalEditProducts = ({ title, data, onProductChanged }) => {
  const [show, setShow] = useState(false);
  const [newData, setNewData] = useState([]);

  useEffect(() => {
    setNewData(data);
  }, [data, show]);

  const handleClose = () => setShow(false);
  const handleShow = () => setShow(true);

  const handleProductChange = (index, name, value) => {
    setNewData((prevValues) => {
      const newValues = [...prevValues];
      newValues[index] = Object.assign({}, newValues[index], { [name]: value });
      return newValues;
    });
  };

  const handleProductAdded = () => {
    setNewData((prevValues) => [...prevValues, { product: "", price: 0 }]);
  };

  const handleProductRemoved = (index) => {
    setNewData((prevValues) => {
      const newValues = [...prevValues];
      newValues.splice(index, 1);
      return newValues;
    });
  };

  const handleSubmitProducts = (e) => {
    e.preventDefault();
    onProductChanged(newData);
    setShow(false);
  };

  return (
    <>
      <div className="content_header">
        <div className="content_header_top">
          <div className="header_left">Products</div>
          <div className="header_right">
            <Button className="round-btn" onClick={handleShow}>
              <i className="fa-solid fa-pencil t-14"></i>
            </Button>
          </div>
        </div>
      </div>

      {show && (
        <Modal show={show} onHide={handleClose} size="">
          <Modal.Header closeButton>
            <Modal.Title>{title}</Modal.Title>
          </Modal.Header>
          <Modal.Body>
            <ProductEdit
              data={newData}
              onProductChanged={handleProductChange}
              onProductAdded={handleProductAdded}
              onProductRemoved={handleProductRemoved}
            />
          </Modal.Body>
          <Modal.Footer>
            <Form>
              <Button
                className="btn-skill-complete"
                onClick={handleSubmitProducts}
              >
                Save
              </Button>
            </Form>
          </Modal.Footer>
        </Modal>
      )}
    </>
  );
};

ProductEdit.jsx

const ProductEdit = ({ data, onProductChanged, onProductRemoved, onProductAdded }) => {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    GetProducts(setProducts);
  });

  return (
    {data.length > 0 ? (
        <Row>
          <Col xs={9} md={9}>
            <div className="product-modal-title mb-3">Pick a new product</div>
          </Col>
          <Col xs={3} md={3}>
            <div className="product-modal-title mb-3 text-center">
              Remove/add
            </div>
          </Col>
        </Row>
      ) : null}

      {data.length === 0 ? (
        <Col xs={12} md={12}>
          <Button
            className="btn-st-large t-16 "
            type="button"
            onClick={onProductAdded}
          >
            Add a product
          </Button>
        </Col>
      ) : (
        <>
          {data?.map((inputField, index) => (
            <div key={index}>
              <Row>
                <Col xs={9} md={9}>
                  <Form.Select
                    as={Col}
                    className="mb-3"
                    onChange={(event) => {
                      onProductChanged(index, "product", event.target.value);
                    }}
                    id="product"
                    name="product"
                    value={inputField.product}
                  >
                    <option>Choose product</option>
                    {products.map((item) => {
                      return (
                        <option key={item.id} value={item.name}>
                          {item.name} ({item.price} kr.)
                        </option>
                      );
                    })}
                  </Form.Select>
                </Col>
                <Col xs={3} md={3}>
                  <div className="btn-section">
                    <button
                      type="button"
                      className="round-btn"
                      onClick={onProductAdded}
                    >
                      <i className="fa-solid fa-plus"></i>
                    </button>
                    <button
                      type="button"
                      className="round-btn"
                      onClick={() => onProductRemoved(index)}
                    >
                      <i className="fa-solid fa-minus"></i>
                    </button>
                  </div>
                </Col>
              </Row>
            </div>
          ))}
        </>
      )}
    </Form>
  );
};

Upvotes: 0

Views: 537

Answers (1)

adsy
adsy

Reputation: 11532

Firstly, the products seem static and never change. No need to keep them in state therefore as there's never a need to alter them. You only need to keep something in state if it changes at some point. You can keep them outside the component and reference them. Minor performance improvement but also signals to other people looking at your code they are unchangeable and static.

Secondly the change handler is over-complex. Here you are storing not an array of objects but just 1 simple selected value (the name of the selected item). React Bootstrap is passing you the value of the option only in the event, and not the whole object. Since that value is just a plain string, which is considered a "primitive" in JS, you don't need to worry about any immutable state changes. Strings are copied when you pass them into a function. But then if you only have the name, how to get the price?

To get the price, you can use that selected name to lookup the relevant option and get the price property. find is a useful tool here. It lets you lookup a single item in array according to some truthiness you define.

You suggested in your question storing the price in state as well; and you might wonder why I didn't do this. Actually here, you have no choice since the library only passes you the value of the option. However other libraries would pass you the whole option. Nonetheless, generally speaking, storing only the bare minimum to identify the selection from the source data is better since you avoid a bunch of bugs around keeping the item in sync with the source data. It wouldn't matter here where your options data doesn't change, but it's just good practice. Better to look it up in the canonical source data using a unique identifier.

I also added a default "please select" option. The default state selects this one first (empty string)

Note price will be undefined before the user selects an option since the default "" option does not appear in products. find returns undefined if it looks through the array and no item it loops over is truthy according to the passed condition.

This is why I used ? (optional chaining) after the find when accessing price. You'd otherwise get an error.


const products = [
    { name: "TV", price: 1000 },
    { name: "Phone", price: 3000 },
]

const MyForm = () => {
    const [selectedName, setSelectedName] = useState("");

    const price = products.find(product => product.name === selectedName)?.price

    return (
        <Form>
           <Form.Select
               onChange={(event) => {setSelectedName(event.target.value);}}
               value={selectedName}
           >
              <option value="">Select option...</option>
              {products.map((item) => {
                 return <option value={item.name}>{item.name}</option>;
              })}
           </Form.Select>
        </Form>
    );
}

Upvotes: 1

Related Questions