mstaffer
mstaffer

Reputation: 65

JSX Not Updating In Real Time When Checkboxes Checked In React

Attempting to do a recipe app, where once you check the boxes, the recipe array updates, and the counter at the bottom is updated as you check them. So far though, I have only been able to do one or the other, if I add the ingredient to the array, I can't update the counter, and if I update the counter, I cannot update the array. Here's what I have so far.

import React from 'react'; 

function IngredientsCheckBoxes(props) {

    let newData = props.ingredients
    let newestArray = []
    let handleOnChange = (e) => {
        let isChecked = e.target.checked;
        if (isChecked) {
            for ( let i = 0; i <= newestArray.length; i++ ){
                if (e.target.value !== i) {
                    newestArray.push(e.target.value)
                    return newestArray
                }
            }
        } else if (isChecked === false) {
            newestArray = newestArray.filter(
                ingred => ingred !== e.target.value
            )
        }
    }

return ( 
    <div>
        <ul className="toppings-list">
            {newData.map(( name , index) => {
                return (
                    <li key={index}>
                        <div className="toppings-list-item">
                            <div className="left-section">
                                <input
                                    type="checkbox"
                                    id={`custom-checkbox-${index}`}
                                    name={name}
                                    value={name}
                                    onChange={e => handleOnChange(e)}
                                />
                     <label htmlFor={`custom-checkbox-${index}`}>{name}</label>
                                </div>
                            </div>
                        </li>
                    );
                })}
            </ul>
            <h1>{count goes here}</h1>
        </div>
    )
}

export default IngredientsCheckBoxes;

I previously used a useState hook, but where ever I put it in the handleOnChange function, it takes over and won't put the ingredient in the array.

I have a feeling the answer is super obvious but after looking at the code for a while I'm looking for outside help, any of which would be appreciated. Thank you in advance.

Upvotes: 2

Views: 1459

Answers (2)

Yevhen Horbunkov
Yevhen Horbunkov

Reputation: 15540

You're supposed to use state to keep track of what's changing inside your component.

Also,

  • you should avoid using array index as a key
  • and would be much better off addressing your items by unique id (name) rather than rely on their position inside array (that's being constantly changed)
  • and drop dynamically generated id's, as odds of never using those for referring to specific DOM node are quite high
  • breaking your UI into more granular (reusable) components would be good for your app

Distilled example of what you're (seemingly) trying to achieve, may look as follows:

const { useState } = React,
      { render } = ReactDOM,
      rootNode = document.getElementById('root')
      
const CheckListItem = ({onHit, label}) => {
  return (
    <label>
      <input type="checkbox" onChange={() => onHit(label)} />
      {label}
    </label>
  )
}

const CheckList = ({itemList}) => {
  const [selectedItems, setSelectedItems] = useState(
    itemList
      .reduce((acc, itemName) => 
        (acc[itemName] = false, acc), {})
  )
  const onCheckItem = itemName => 
    setSelectedItems({...selectedItems, [itemName]: !selectedItems[itemName]})
  const numberOfChosenItems = Object
    .values(selectedItems)
    .filter(Boolean)
    .length
  const listOfChosenItems = Object
    .keys(selectedItems)
    .filter(itemName => selectedItems[itemName] === true)
    .join(', ')
  

  return !!itemList.length && (
    <div>
      <ul>
        {
          itemList.map(itemName => (
            <li key={itemName}>
              <CheckListItem
                label={itemName}
                onHit={onCheckItem}
              />
            </li>
          ))
        }
      </ul>
      {
        !!numberOfChosenItems && (
          <div>
            Chosen items: {listOfChosenItems} (Total: {numberOfChosenItems})
          </div>
        )
      }
    </div>
  )
}

const ingredients = ['flour', 'eggs', 'milk', 'sugar']

const App = () => {
  return (
    <CheckList itemList={ingredients} />
  )
}

render (
  <App />,
  rootNode
)
li {
  display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>

Upvotes: 2

Sarthak Malik
Sarthak Malik

Reputation: 125

newestArray should be a state in order to re-render IngredientsCheckBoxes component and subsequently show changes in the component view.

const [selectedIngredients, setSelectedIngredients] = useState([]);

And then use selectedIngredients and setSelectedIngredients to use and update state respectively.

Here's a codesandbox with the working code.

Make sure you use suitable names for state. selectedIngredients is a better name than newestArray as it does tell you about what it actually means.

Upvotes: 1

Related Questions