0

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>
  );
};
  • Why not set the option value to "item" instead of "item.name", and that way your onChange has access to the entire object? – C. Helling Sep 13 '22 at 19:22
  • That should probably work. I hadn't thought about just adding the item, but the problem is that I can't figure out how to fetch the ```price``` value in the ```onChangeProduct```. I quite new to react so any help is appreciated – Alexander Simonsen Sep 14 '22 at 07:11
  • If you add a `console.log(value)` to your `onChange`, or `console.log(JSON.stringify(value))`, what do you see? I assume you would access the price from something like `value.price` or something like that. – C. Helling Sep 14 '22 at 16:29
  • Doing this I'll just get an empty array ```[object Object]``` in the console. I've tried to access it by catching value.price but failed. It seems that catching the ```item``` isn't possible if I want to get the values of price and name. – Alexander Simonsen Sep 16 '22 at 12:47

1 Answers1

1

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>
    );
}
halfer
  • 19,824
  • 17
  • 99
  • 186
adsy
  • 8,531
  • 2
  • 20
  • 31
  • This works if I want to show the value of a single product, so thanks. But I found out that I need to make some changes to the function. I want/need to make a function where the user can add/remove products from their card, and therefore the price function should catch the price of a product with a certain index. I used the help you gave me last at [old post](https://stackoverflow.com/questions/73649626/usestate-resets-to-init-state-in-crud-function-when-edit-reactstars-component) - I've also made an edit here. Please tell me if I still can use this answer or if I need to go another way. – Alexander Simonsen Sep 17 '22 at 13:11
  • Yeh! It should be fine. But you will also need to move the lookup in scope. I will edit my answer – adsy Sep 17 '22 at 21:45
  • Actually could you also show the parent component and where the selected product is stored? – adsy Sep 17 '22 at 21:49
  • Were you able to find the issue here? I have tried back and forth but haven't been able to fix the issue yet :( – Alexander Simonsen Sep 24 '22 at 07:03