JSX Not Updating In Real Time When Checkboxes Checked In React - reactjs

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.

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>

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.

Related

useState Hook is not working for me . I am trying to update the array of objects in props.data i am getting the array array rendering is not working

The useState hook is not working for me. In props.data I am getting the array.
I am trying to update the array of objects, but rendering is not working:
const MenuList = (props)=>{
const [enteredData,setEnteredData] = useState(props.data);
const amountChangeHandler = (event)=>{
const findElementId = event.target.attributes.alter.value;
const item = props.data.find(item => item.id === findElementId);
item.amount = parseInt(document.getElementById(findElementId).value);
console.log(props.data);
setEnteredData(props.data);
}
return(
<ul>
{enteredData.map((item)=><li key={item.id} className={classes['cart-item']}>
<div>
<h2>{item.mealName}</h2>
<div className={classes.summary}><i>{item.mealDescription}</i>
<span className={classes.price}>${item.price}</span>
<span className={classes.amount}>x {item.amount}</span>
</div>
</div>
<div className={classes.actions}>
<div><label htmlFor={item.id}>Amount </label>
<input type='number' id={item.id} defaultValue='1' ></input>
</div>
<button alter={item.id} onClick={amountChangeHandler}>+Add</button>
</div>
</li>) }</ul>
)
}
export default MenuList;
This is because react is not identifying that your array has changed. Basically react will assign a reference to the array when you define it. But although you are changing the values inside the array, this reference won't be changed. Because of that component won't be re rendered.
So you have to notify react, that the array has updated.
This can be achieve by updating the enteredData by iterating the props.data
const [enteredData,setEnteredData] = useState(props.data);
const amountChangeHandler = (event)=>{
const findElementId = event.target.attributes.alter.value;
const item = props.data.find(item => item.id === findElementId);
item.amount = parseInt(document.getElementById(findElementId).value);
console.log(props.data);
setEnteredData([...props.data]);
}
return(
<ul>
{enteredData.map((item)=><li key={item.id} className={classes['cart-item']}>
<div>
<h2>{item.mealName}</h2>
<div className={classes.summary}><i>{item.mealDescription}</i>
<span className={classes.price}>${item.price}</span>
<span className={classes.amount}>x {item.amount}</span>
</div>
</div>
<div className={classes.actions}>
<div><label htmlFor={item.id}>Amount </label>
<input type='number' id={item.id} defaultValue='1' ></input>
</div>
<button alter={item.id} onClick={amountChangeHandler}>+Add</button>
</div>
</li>) }</ul>
)
Issue
The issue here is state/reference mutation.
// Currently `enteredData` is a reference to `props.data
const [enteredData, setEnteredData] = useState(props.data);
const amountChangeHandler = (event) => {
const findElementId = event.target.attributes.alter.value;
// `item`, if found, is a reference to an element in the
// `enteredData`, a.k.a. `props.data`, array
const item = props.data.find(item => item.id === findElementId);
// This is the mutation(!!), directly mutating the item reference
item.amount = parseInt(document.getElementById(findElementId).value);
// props.data is still the same reference as `enteredData`
setEnteredData(props.data);
};
Direct mutation of existing state.
A new array reference is never created for React's reconciliation process to use shallow reference equality to trigger a rerender.
Mutating the props.data array in the child also mutates the source in the parent component, which should be avoided at all costs.
Solution
Use a functional state update to correctly access, and update from, the previous state value. You should shallow copy all state, and nested state, that is being updated.
Example:
const [enteredData, setEnteredData] = useState(props.data);
const amountChangeHandler = (event) => {
const findElementId = event.target.attributes.alter.value;
setEnteredData(data => data.map(
item => item.id === findElementId
? {
...item,
amount: parseInt(document.getElementById(findElementId).value, 10)
}
: item)
);
};

Drop down for a single element in a loop based on an id in react

I'm new to react and have an app that displays some data. I am using a map function to build one component multiple times. When a button is clicked inside of an element more data should be displayed but only in the clicked element. Currently, when I click a button in one element can toggle the display of the additional data for all element as well as store the unique id of the clicked element in a state. I am pretty sure that I need to filter the results and I have seen similar examples but I can't say that I fully understand them. Any tips or more beginner-friendly tutorials are greatly appreciated.
import React, { useState, useEffect } from 'react';
import '../style/skeleton.css'
import '../style/style.css'
export default function Body( student ) {
const [active, setActive] = useState({
activeStudent: null,
});
const [display, setDisplay] = useState(true)
useEffect(() => {
if (display === false) {
setDisplay(true)
} else {
setDisplay(false)
}
}, [active])
const handleClick = (id) => setActive({ activeStudent: id});
return (
<div>
{student.student.map((data) => {
const id = data.id;
return (
<div key={data.id} className="row border">
<div className="two-thirds column">
<h3>{data.firstName} {data.lastName}</h3>
{ display ?
<button onClick={() => handleClick(id)}>-</button>
:
<button onClick={() => handleClick(id)}>+</button> }
{ display ? <div>
<p>{data.addional} additonal data</p>
</div> : null }
</div>
</div>
)
})}
</div>
);
}
Change your code from:
{ display ? <div><p>{data.addional} additonal data</p></div> : null }
To:
{ active.activeStudent === id ? <div><p>{data.addional} additonal data</p></div> : null }

React useRef to set the width of a DOM node

so I'm facing an issue where I am not able to change the width of DOM node using useRef. Im using the onDragEnd event to trigger the change of the width on the selected node only.
I'm setting the width by changing the 'elementRef.current.style.width property. But the change is not being reflected on the frontend.
Heres my code:
import React, { useEffect, useState, useRef } from "react";
import timelineItems from "../timelineItems";
import "../index.css";
const TimeLine = () => {
const [sortedTimeline, setTimelineSorted] = useState([]);
const increaseDateDomRef = useRef(null);
let usedIndex = [];
useEffect(() => {
let sortedResult = timelineItems.sort((a, b) => {
return (
new Date(a.start) -
new Date(b.start) +
(new Date(a.end) - new Date(b.end))
);
});
setTimelineSorted(sortedResult);
}, []);
const increaseEndDate = (e) => {
};
const increaseEndDateFinish = (e, idx) => {
//Im setting the width here but it is not being reflected on the page
increaseDateDomRef.current.style.width = '200px';
console.log(increaseDateDomRef.current.clientWidth);
};
return (
<div className="main">
{sortedTimeline.map((item, idx) => {
return (
<div key={idx}>
<p>{item.name}</p>
<p>
{item.start} - {item.end}
</p>
<div className="wrapper">
<div className="circle"></div>
<div
className="lineDiv"
ref={increaseDateDomRef}
draggable
onDragStart={(e) => increaseEndDate(e)}
onDragEnd={(e) => increaseEndDateFinish(e, idx)}
>
<hr className="line" />
</div>
<div className="circle"></div>
</div>
</div>
);
})}
</div>
);
};
export default TimeLine;
first of all this may not be working because you are using a single reference for multiple elements.
This answer on another post may help you https://stackoverflow.com/a/65350394
But what I would do in your case, is something pretty simple.
const increaseEndDateFinish = (e, idx) => {
const target = e.target;
target.style.width = '200px';
console.log(target.clientWidth);
};
You don't have to use a reference since you already have the reference on the event target.

Is this the correct way to set state using react's useState hook

I was looking into React hooks and wasn't sure how useState works with arrays. So I made a working example with 3 controlled inputs, fed by an array. It works but I was wondering if this is the correct/optimal way of doing this.
Working example on codepen
const { useState } = React;
const InputAndButton = ({ tag, index, handleTagChange }) => (
<div>
<input value={tag} onChange={(e) => handleTagChange(e.target.value, index)} />
<button type="button" onClick={() => handleTagChange('', index)}>×</button>
</div>
)
function Tags(){
const [ tags, setTags ] = useState(["", "", ""]);
const handleTagChange = (value, index) => {
// make copy of state
const tagsCopy = [...tags];
// change the [i] value to the new one given
tagsCopy[index] = value;
// set copy back as state
setTags(tagsCopy);
}
return(
<form>
tags
{tags.map((tag, i) => <InputAndButton tag={tag} index={i} handleTagChange={handleTagChange} key={i} />)}
</form>
)
}
ReactDOM.render(<Tags />, document.getElementById("app"));
Can anybody give me some feedback here?

Why Todo List in React erases always the last item?

I've been trying to make a Todo List App Work with React Hooks.
Everything works just fine when I use <span>{todo}</span>. It just delete the element that I click. But when I change <span>{todo}</span> for <input></input>, every 'X' that I click to delete always delete the last element. I just don't know what's happening, as the keys aren't changed.
Todo.js Component:
import React, { useState } from 'react';
const TodoForm = ({ saveTodo }) => {
const[value, setValue] = useState('');
return (
<form
onSubmit={event => {
event.preventDefault();
saveTodo(value);
setValue('');
}}
>
<input onChange={event => setValue(event.target.value)} value={value} />
</form>
)
}
const TodoList =({ todos, deleteTodo }) => (
<ul>
{
todos.map((todo, index) => (
<li key={index.toString()}>
<span>{todo}</span>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
))
}
</ul>
);
const Todo = () => {
const [todos, setTodos] = useState([]);
return (
<div className="App">
<h1>Todos</h1>
<TodoForm saveTodo={todoText => {
const trimmedText = todoText.trim();
if(trimmedText.length > 0) {
setTodos([...todos, trimmedText]);
}
}}
/>
<TodoList
todos={todos}
deleteTodo={todoIndex => {
const newTodos = todos.filter((_, index) => index !== todoIndex);
setTodos(newTodos);
}}
/>
</div>
);
};
export default Todo;
It changes the deletion behavior when I change:
<li key={index.toString()}>
<span>{todo}</span>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
to:
<li key={index.toString()}>
<input></input>
<button onClick={() => deleteTodo(index)}>X</button>
</li>
Are you sure that this is the case? Or it just seems to behave that way, because you render the input without any values? If I paste your code (and adjust the input to actually include the value of the todo) into a CodeSandbox it deletes the correct element. Please also consider that using indexes as list keys should be seen as the "last resort" (See React docs).
The problem is that you are using index as the key. Therefore the last item (key that stops existing) is removed and all the other items are just updated.
Instead, create some kind of unique id for your todos and use that id as key.

Resources