useState array as dependency causing infinite loop - reactjs

I have been struggling with this for hours, i'm new to React and would appreciate any assistance.
I'm working on something where users can pick regions into an array.
My main problem is that i want the array that users choose to have unique values only.
I have tried using a javascript SET but that can't be mapped through. The array will be mapped through then displayed to the user.
And i have tried setting "if" statements, that check for duplicate values, inside useEffect but the dependency on a useState array creates an infinite loop.
I have read about using useRef on an array to avoid useEffect infinite loops but i find that its normally for static rather than changing arrays.
Below is the important part:
const [regions, setRegions] = useState([]);
const [region, setRegion] = useState("");
useEffect(() => {
if (region) {
if (regions.includes.region) {
return;
} else if (!regions.includes.region) {
setRegions((prevValue) => {
return [...prevValue, region];
});
}
}
// setRegions((previousState) => new Set([...previousState, region]));
}, [region, regions]);
The rest of the code for context:
import { useEffect, useState } from "react";
import PlacesAutocomplete, {
geocodeByAddress,
getLatLng,
} from "react-places-autocomplete";
export default function Test() {
const [address, setAddress] = useState("");
const [coordinate, setCoordinates] = useState({
lat: null,
lng: null,
});
const [regions, setRegions] = useState([]);
const [region, setRegion] = useState("");
useEffect(() => {
if (region) {
if (regions.includes.region) {
return;
} else if (!regions.includes.region) {
setRegions((prevValue) => {
return [...prevValue, region];
});
}
}
// setRegions((previousState) => new Set([...previousState, region]));
}, [region, regions]);
const handleSelect = async (value) => {
const result = await geocodeByAddress(value);
const full_region = result[0].formatted_address;
const part_region = full_region.substring(0, full_region.indexOf(","));
let province = "";
if (result[0].address_components[2].short_name.length <= 3) {
province = result[0].address_components[2].short_name;
} else {
province = result[0].address_components[3].short_name;
}
setAddress(value);
setCoordinates(coordinate);
setRegion(part_region.concat("-", province));
};
const onDelete = (e) => {
const value = e.target.getAttribute("value");
console.log("onDelete: ", value);
setRegions(regions.filter((item) => item !== value));
};
// setRegions(Array.from(new Set(regions)));
return (
<>
<PlacesAutocomplete
value={address}
onChange={setAddress}
onSelect={handleSelect}
searchOptions={{
componentRestrictions: { country: ["za"] },
types: ["(regions)"],
}}
>
{({ getInputProps, suggestions, getSuggestionItemProps, loading }) => (
<div>
<input
{...getInputProps({
placeholder: "Add regions...",
className: "location-search-input",
})}
/>
<div className="autocomplete-dropdown-container">
{loading && <div>Loading...</div>}
{suggestions.map((suggestion) => {
const className = suggestion.active
? "suggestion-item--active"
: "suggestion-item";
// inline style for demonstration purpose
const style = suggestion.active
? { backgroundColor: "orange", cursor: "pointer" }
: { backgroundColor: "silver", cursor: "pointer" };
return (
<div
key={suggestion.description}
{...getSuggestionItemProps(suggestion, {
className,
style,
})}
>
<span>{suggestion.description}</span>
</div>
);
})}
</div>
</div>
)}
</PlacesAutocomplete>
<p>Regions</p>
<ul>
{regions.map((region) => (
<li
// key={region}
title="remove"
className="cursor-pointer"
onClick={onDelete}
value={region}
>
{region}
</li>
))}
</ul>
</>
);
}

You are using .includes() incorrectly by trying to obtain region as a property: regions.includes.region
This results in:
the second condition else if (!regions.includes.region) always succeeding,
which then results in the state change setRegions() being made,
which then triggers the [regions] in the dependency,
which then loops the useEffect() again, and again.. ..infinitely.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
Instead, it should be passed as a parameter to the method: if(regions.includes(region)) and if(!regions.includes(region))
useEffect(() => {
if (region) {
if (regions.includes(region)) {
return;
}
if (!regions.includes(region)) {
setRegions((prevValue) => {
return [...prevValue, region];
});
}
}
}, [region, regions]);
You could probably also simplify it by only modifying the state if the condition doesn't succeed:
useEffect(() => {
if (region) {
if (!regions.includes(region)) {
setRegions((prevValue) => {
return [...prevValue, region];
});
}
// else do nothing
}
}, [region, regions]);

Related

React | Collect State Values of Children Array and update Per Object and Save to PouchDB

Stackoverflow
problem
I have separate components that house Tiptap Editor tables. At first I had a save button for each Child Component which worked fine, but was not user friendly. I want to have a unified save button that will iterate through each child Table component and funnel all their editor.getJSON() data into an array of sections for the single doc object . Then finish it off by saving the whole object to PouchDB
What did I try?
link to the repo โ†’ wchorski/Next-Planner: a CRM for planning events built on NextJS (github.com)
Try #1
I tried to use the useRef hook and the useImperativeHandle to call and return the editor.getJSON(). But working with an Array Ref went over my head. I'll post some code of what I was going for
// Parent.jsx
const childrenRef = useRef([]);
childrenRef.current = []
const handleRef = (el) => {
if(el && !childrenRef.current.includes(el)){
childrenRef.current.push(el)
}
}
useEffect(() =>{
childrenRef.current[0].childFunction1() // I know this doesn't work, because this is where I gave up
})
// Child.jsx
useImperativeHandle(ref, () => ({
childFunction1() {
console.log('child function 1 called');
},
childFunction2() {
console.log('child function 2 called');
},
}))
Try #2
I set a state counter and passed it down as a prop to the Child Component . Then I update the counter to trigger a child function
// Parent.jsx
export const Planner = ({id, doc, rev, getById, handleSave, db, alive, error}) => {
const [saveCount, setSaveCount] = useState(0)
const handleUpdate = () =>{
setSaveCount(prev => prev + 1)
}
const isSections = () => {
if(sectionsState[0]) handleSave(sectionsState)
if(sectionsState[0] === undefined) console.log('sec 0 is undefined', sectionsState)
}
function updateSections(newSec) {
setsectionsState(prev => {
const newState = sectionsState.map(obj => {
if(!obj) return
if (obj.header === newSec.header) {
return {...obj, ...newSec}
}
// ๐Ÿ‘‡๏ธ otherwise return object as is
return obj;
});
console.log('newState', newState);
return newState;
});
}
useEffect(() => {
setsectionsState(doc.sections)
}, [doc])
return (<>
<button
title='save'
className='save'
onPointerUp={handleUpdate}>
Save to State <FiSave />
</button>
<button
style={{right: "0", width: 'auto'}}
title='save'
className='save'
onClick={isSections}>
Save to DB <FiSave />
</button>
{doc.sections.map((sec, i) => {
if(!sec) return
return (
<TiptapTable
key={i}
id={id}
rev={doc.rev}
getById={getById}
updateSections={updateSections}
saveCount={saveCount}
section={sec}
db={db}
alive={alive}
error={error}
/>
)
})}
</>)
// Child.jsx
export const TiptapTable = ((props, ref) => {
const {id, section, updateSections, saveCount} = props
const [currTimeStart, setTimeStart] = useState()
const [defTemplate, setdefTemplate] = useState('<p>loading<p>')
const [isLoaded, setIsLoaded] = useState(false)
const [notesState, setnotesState] = useState('')
const editor = useEditor({
extensions: [
History,
Document,
Paragraph,
Text,
Gapcursor,
Table.configure({
resizable: true,
}),
TableRow.extend({
content: '(tableCell | tableHeader)*',
}),
TableHeader,
TableCell,
],
// i wish it was this easy
content: (section.data) ? section.data : defTemplate,
}, [])
const pickTemplate = async (name) => {
try{
const res = await fetch(`/templates/${name}.json`,{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
});
const data = await res.json()
setIsLoaded(true)
setdefTemplate(data)
console.log('defTemplate, ', defTemplate);
// return data
} catch (err){
console.warn('template error: ', err);
}
}
function saveData(){
console.log(' **** SAVE MEEEE ', section.header);
try{
const newSection = {
header: section.header,
timeStart: currTimeStart,
notes: notesState,
data: editor.getJSON(),
}
updateSections(newSection)
} catch (err){
console.warn('table update error: ', id, err);
}
}
useEffect(() => {
// ๐Ÿ‘‡๏ธ don't run on initial render
if (saveCount !== 0) saveData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
useEffect(() => {
setTimeStart(section.timeStart)
setnotesState(section.notes)
if(!section.data) pickTemplate(section.header).catch(console.warn)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, section, isLoaded])
useEffect(() => {
if (editor && !editor.isDestroyed) {
if(section.data) editor.chain().focus().setContent(section.data).run()
if(!section.data) editor.chain().focus().setContent(defTemplate).run()
setIsLoaded(true)
}
}, [section, defTemplate, editor]);
if (!editor) {
return null
}
return isLoaded ? (<>
<StyledTableEditor>
<div className="title">
<input type="time" label='Start Time' className='time'
onChange={(e) => setTimeStart(e.target.value)}
defaultValue={currTimeStart}
/>
<h2>{section.header}</h2>
</div>
<EditorContent editor={editor} className="tiptap-table" ></EditorContent>
// ... non relavent editor controls
<button
title='save'
className='save2'
onPointerUp={() => saveData()}>
Save <FiSave />
</button>
</div>
</nav>
</StyledTableEditor>
</>)
: null
})
TiptapTable.displayName = 'MyTiptapTable';
What I Expected
What I expected was the parent state to update in place, but instead it overwrites the previous tables. Also, once it writes to PouchDB it doesn't write a single piece of new data, just resolved back to the previous, yet with an updated _rev revision number.
In theory I think i'd prefer the useRef hook with useImperativeHandle to pass up the data from child to parent.
It looks like this question is similar but doesn't programmatically comb through the children
I realize I could have asked a more refined question, but instead of starting a new question I'll just answer my own question from what I've learned.
The problem being
I wasn't utilizing React's setState hook as I iterated and updated the main Doc Object
Thanks to this article for helping me through this problem.
// Parent.jsx
import React, {useState} from 'react'
import { Child } from '../components/Child'
export const Parent = () => {
const masterDoc = {
_id: "123",
date: "2023-12-1",
sections: [
{header: 'green', status: 'old'},
{header: 'cyan', status: 'old'},
{header: 'purple', status: 'old'},
]
}
const [saveCount, setSaveCount] = useState(0)
const [sectionsState, setsectionsState] = useState(masterDoc.sections)
function updateSections(inputObj) {
setsectionsState(prev => {
const newState = prev.map(obj => {
// ๐Ÿ‘‡๏ธ if id equals 2, update country property
if (obj.header === inputObj.header)
return {...obj, ...inputObj}
return obj;
});
return newState;
});
}
return (<>
<h1>Parent</h1>
{sectionsState.map((sec, i) => {
if(!sec) return
return (
<Child
key={i}
section={sec}
updateSections={updateSections}
saveCount={saveCount}
/>
)
})}
<button
onClick={() => setSaveCount(prev => prev + 1)}
>State dependant update {saveCount}</button>
</>)
}
// Child.jsx
import React, {useEffect, useState, forwardRef, useImperativeHandle} from 'react'
export const Child = forwardRef((props, ref) => {
const {section, updateSections, saveCount} = props
const [statusState, setStatusState] = useState(section.status)
function modData() {
const obj = {
header: section.header,
status: statusState
}
updateSections(obj)
}
useEffect(() => {
// ๐Ÿ‘‡๏ธ don't run on initial render
if (saveCount !== 0) modData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveCount])
return (<>
<span style={{color: section.header}}>
header: {section.header}
</span>
<span>status: {section.status}</span>
<input
defaultValue={section.status}
onChange={(e) => setStatusState(e.target.value)}
/>
________________________________________
</>)
})
Child.displayName = 'MyChild';

Why am I getting this error: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks?

I'm new to react but I've encountered enough errors to know not to call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks
I'm trying to import data from mongodb atlas into my app.
From the codes written below, I don't think it has violated the rule. Any help would be great thx!
import { useState, useEffect } from "react";
import axios from "axios";
const Events = () => {
const [allEvents, setEvents] = useState([]);
const fetchEvents = async () => {
try {
const { data } = await axios.get(`http://localhost:5000/get/events`);
return data
} catch (err) {
console.log(err);
}
};
useEffect(() => {
fetchEvents();
}, []);
};
export default Events;
function CalendarComponent() {
//useState declarations
const [newEvent, setNewEvent] = useState({
id: "",
title: "",
start: "",
action: "",
});
const [allEvents, setEvents] = useState(events);
const [toggleAdd, setToggleAdd] = useState(false);
const [msg, setMsg] = useState("");
//handle creating new event on button click
function handleAddSlot() {
const title = newEvent.title;
const start = newEvent.start;
const end = start;
let success = 0;
if (title && newEvent.action === control.ADD) {
// add new event into the list
const id = allEvents.length;
if(data.Events.some(i=>i["Serial Number"]===title)){
// let newArr = [...data.Events]
// const res = newArr.find(e=>e["Serial Number"]===title)["Scheduled Sample Date"]
// console.log(res);
// res.push({id:res.length,start,end})
data.Events.map(item=>item["Serial Number"]===title?{...item}["Scheduled Sample Date"].push({"id":{...item}["Scheduled Sample Date"].length,start}):item)
}
else{
setEvents((prev) => [...prev, { start, end, title }]);
}
setToggleAdd(true);
console.log(data.Events)
success = 1;
}
if (newEvent.action === control.UPDATE) {
// find id of existing event to update
setEvents((currEvents) =>
currEvents.map((event) => {
if (event.id === newEvent.id) {
return {
...event,
start: start,
end: start,
title: title,
};
} else {
return event;
}
})
);
success = 1;
console.log(allEvents);
}
// if action successful, close modal and clear all fields
if (success === 1) {
setToggleAdd(false);
clearFields();
}
}
// handle when user select existing scope event
function handleSelectEvent(event) {
setMsg("Update Scope");
const start = new Date(event.start);
setNewEvent({
...newEvent,
action: control.UPDATE,
title: event.title,
start,
id: event.id,
});
setToggleAdd(true);
}
//handle calendar slot click
function handleClick(e) {
newEvent.start = e.start;
newEvent.action = control.ADD;
setMsg("Add New Scope");
if (
// ? Can be better written
document.getElementById("id01")?.classList.contains("setDisplay") === true
) {
setToggleAdd(true);
}
}
//handle modal close when user clicks on x
function toggleClick() {
if (toggleAdd === true) {
setToggleAdd(false);
clearFields();
}
}
//function to clear all fields
function clearFields() {
newEvent.start = "";
newEvent.title = "";
}
return (
<div className={`calendar-container`}>
<AddModal
id="id01"
toggle={toggleAdd}
newEvent={newEvent}
toggleClick={toggleClick}
handleSelectSlot={handleAddSlot}
setNewEvent={setNewEvent}
msg={msg}
/>
<Calendar
localizer={localizer}
events={allEvents}
popup
defaultView="month"
longPressThreshold={1}
views={["month"]}
eventPropGetter={(event) => {
const identifier = data.Events.find(
(item) => item["Type"] === event.type
);
const backgroundColor = identifier
? data.Color.find((item) => item[`${identifier.Type}`])[
`${identifier.Type}`
]
: "";
return { style: { backgroundColor } };
}}
onSelectEvent={handleSelectEvent}
onSelectSlot={(e) => handleClick(e)}
selectable
style={{ height: 500, margin: "30px" }}
components={{
toolbar: CustomToolbar,
}}
/>
<div className="calendar-sidebar">
<Button text="Add New" button="button" onClick={handleClick} />
<Log type='Schedule' />
</div>
</div>
);
}
I've attached the CalendarComponent. I can't really seem to find the problem in CalendarComponent myself. Hopefully it's not in the react big calendar package and I'm not missing something

why console gives in this sequence?

I am making a mern ecommerce website i just want to see how useEffect works so i console.log in some part of useEffect and loadFilteredResults i saw that --->
initial
entry
skip
entry1
screen shot
but i think it shoud be-->
initial
entry
entry1
skip
why console give this?? i am a begginer, i am a self learner , so please if you need any extra info please comment.
code snippet-->
const loadFilteredResults = (newFilters) => {
console.log("entry")
getFilteredProducts(skip, limit, newFilters).then((data) => {
console.log("entry1")
if (data.error) {
setError(data.error);
} else {
//console.log(data);
setFilteredResults(data.data);
//console.log("size-->");
//console.log(data.size);
setSize(data.size);
setSkip(0);
}
});
};
....
....
useEffect(() => {
init();
console.log("initial");
loadFilteredResults(skip, limit, myFilters.filters);
console.log("skip");
}, []);
//full code of shop.js
import React, { useEffect, useState } from "react";
import Layout from "./Layout";
import Card from "./Card";
import { getCategories, getFilteredProducts } from "./apiCore";
import Checkbox from "./Checkbox";
import RadioBox from "./RadioBox";
import { prices } from "./fixedPrices";
const Shop = () => {
const [myFilters, setMyFilters] = useState({
filters: { category: [], price: [] }
});
const [categories, setCategories] = useState([]);
const [error, setError] = useState(false);
const [limit, setLimit] = useState(3);//prpduct lesss so use 3 but sir used 6
const [skip, setSkip] = useState(0);
const [size, setSize] = useState(0);
const [filteredResults, setFilteredResults] = useState([]);
const init = () => {
getCategories().then((data) => {
if (data.error) {
//console.log("error");
setError(data.error);
} else {
//console.log("set");
//console.log(data);
setCategories(data);
//console.log(data);
}
});
};
const loadFilteredResults = (newFilters) => {
//console.log(newFilters);
console.log("entry")
getFilteredProducts(skip, limit, newFilters).then((data) => {
console.log("entry1")
if (data.error) {
setError(data.error);
} else {
//console.log(data);
setFilteredResults(data.data);
//console.log("size-->");
//console.log(data.size);
setSize(data.size);
setSkip(0);
}
});
};
const loadMore = () => {
console.log("skip"+skip);
console.log("limit"+limit);
let toSkip = skip + limit;
console.log("toSkip"+toSkip);
getFilteredProducts(toSkip, limit, myFilters.filters).then((data) => {
//console.log("filter");
//console.log( myFilters.filters)
if (data.error) {
setError(data.error);
} else {
//console.log(filteredResults);
//console.log(data.data);
setFilteredResults([...filteredResults, ...data.data]);
//console.log("after");
//console.log(...filteredResults);
//console.log(filteredResults);
//console.log(filteredResults);
//console.log([...filteredResults])
//console.log([...filteredResults, ...data.data])
setSize(data.size);
setSkip(toSkip);
}
});
};
const loadMoreButton = () => {
return (
size > 0 &&
size >= limit && (
<button onClick={loadMore} className="btn btn-warning mb-5">
load more
</button>
)
);
};
useEffect(() => {
init();
//console.log(skip);
console.log("initial");
loadFilteredResults(skip, limit, myFilters.filters);
console.log("skip");
}, []);
const handleFilters = (filters, filterBy) => {
//console.log("SHOP", filters, filterBy);
const newFilters = { ...myFilters };
//console.log(newFilters);
newFilters.filters[filterBy] = filters;
//console.log(typeof(filters));
if (filterBy === "price") {
let priceValues = handlePrice(filters);
newFilters.filters[filterBy] = priceValues;
//console.log(priceValues);
}
//console.log(myFilters.filters);
loadFilteredResults(myFilters.filters);
setMyFilters(newFilters);
};
const handlePrice = (value) => {
const data = prices;
let array = [];
//console.log(value);
for (let key in data) {
if (data[key]._id === parseInt(value)) {
array = data[key].array;
}
}
return array;
};
// const x = (filters)=>{
// console.log("filters:"+filters);
// handleFilters(filters, "category")
// }
return (
<Layout
title="Shop Page"
description="search and buy books of your choice"
className="container-fluid"
>
<div className="row">
<div className="col-4">
<h4>Filter by categories</h4>
<ul>
{/* below will be show in list show we wrap it in unorder list */}
<Checkbox
categories={categories}
handleFilters={(filters) =>
handleFilters(filters, "category")
}
/>
</ul>
<h4>Filter by price range</h4>
<div>
<RadioBox
prices={prices}
handleFilters={(filters) => handleFilters(filters, "price")}
/>
</div>
</div>
<div className="col-8">
<h2 className="mb-4">Products</h2>
<div className="row">
{filteredResults.map((product, i) => (
<Card key={i} product={product} />
))}
</div>
<hr />
{loadMoreButton()}
</div>
</div>
</Layout>
);
};
export default Shop;
getFilteredProducts must be a Promise. Please read Using promises
Callbacks added with then() will never be invoked before the
completion of the current run of the JavaScript event loop.

Useeffect not update after i select new item on Select option

I changed option but useeffect not update input. Please guide me where i make mistake. First i use useEffect to setCurrency after that i use mapping for getCurrency to add it on Select option. onCitySelect i added it to setSelectedId when i change Select option. Lastly, i tried to get address with api but the problem is i need to change api/address?currency=${selectId.id}` i added selectedId.id but everytime i change option select it is not affect and update with useEffect. I tried different solution couldn't do it. How can i update useEffect eveytime option select change (selectId.id) ?
export default function Golum() {
const router = useRouter();
const dispatch = useDispatch();
const [getCurrency, setCurrency] = useState("");
const [getAddress, setAddress] = useState("");
const [selectCity, setSelectCity] = useState("");
const [selectId, setSelectId] = useState({
id: null,
name: null,
min_deposit_amount: null,
});
const [cityOptions, setCityOptions] = useState([]);
useEffect(() => {
setSelectCity({ label: "Select City", value: null });
setCityOptions({ selectableTokens });
}, []);
const onCitySelect = (e) => {
if (e == null) {
setSelectId({
...selectId,
id: null,
name: null,
min_deposit_amount: null,
});
} else {
setSelectId({
...selectId,
id: e.value.id,
name: e.value.name,
min_deposit_amount: e.value.min_deposit_amount,
});
}
setSelectCity(e);
};
const selectableTokens =
getCurrency &&
getCurrency.map((value, key) => {
return {
value: value,
label: (
<div>
<img
src={`https://central-1.amazonaws.com/assets/icons/icon-${value.id}.png`}
height={20}
className="mr-3"
alt={key}
/>
<span className="mr-3 text-uppercase">{value.id}</span>
<span className="currency-name text-uppercase">
<span>{value.name}</span>
</span>
</div>
),
};
});
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/currencies`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setCurrency(data);
});
}
return () => (mounted = false);
}, []);
useEffect(() => {
const api = new Api();
let mounted = true;
if (!localStorage.getItem("ACCESS_TOKEN")) {
router.push("/login");
}
if (mounted && localStorage.getItem("ACCESS_TOKEN")) {
api
.getRequest(
`${process.env.NEXT_PUBLIC_URL}api/address?currency=${selectId.id}`
)
.then((response) => {
const data = response.data;
dispatch(setUserData({ ...data }));
setAddress(data.address);
})
.catch((error) => {});
}
return () => (mounted = false);
}, []);
return (
<div className="row mt-4">
<Select
isClearable
isSearchable
onChange={onCitySelect}
value={selectCity}
options={selectableTokens}
placeholder="Select Coin"
className="col-md-4 selectCurrencyDeposit"
/>
</div>
<div className="row mt-4">
<div className="col-md-4">
<Form>
<Form.Group className="mb-3" controlId="Form.ControlTextarea">
<Form.Control className="addressInput" readOnly defaultValue={getAddress || "No Address"} />
</Form.Group>
</Form>
</div>
</div>
);
}
The second parameter of the useEffect hook is the dependency array. Here you need to specify all the values that can change over time. In case one of the values change, the useEffect hook re-runs.
Since you specified an empty dependency array, the hook only runs on the initial render of the component.
If you want the useEffect hook to re-run in case the selectId.id changes, specify it in the dependency array like this:
useEffect(() => { /* API call */ }, [selectId.id]);
I think you are accessing the e object wrong. e represents the click event and you should access the value with this line
e.target.value.id
e.target.value.value

Text field should only change for one value and not over the entire list

I have a list and this list has several elements and I iterate over the list. For each list I display two buttons and an input field.
Now I have the following problem: as soon as I write something in a text field, the same value is also entered in the other text fields. However, I only want to change a value in one text field, so the others should not receive this value.
How can I make it so that one text field is for one element and when I write something in this text field, it is not for all the other elements as well?
import React, { useState, useEffect } from 'react'
import axios from 'axios'
function Training({ teamid }) {
const [isTrainingExisting, setIsTrainingExisting] = useState(false);
const [trainingData, setTrainingData] = useState([]);
const [addTraining, setAddTraining] = useState(false);
const [day, setDay] = useState('');
const [from, setFrom] = useState('');
const [until, setUntil] = useState('');
const getTrainingData = () => {
axios
.get(`${process.env.REACT_APP_API_URL}/team/team_training-${teamid}`,
)
.then((res) => {
if (res.status === 200) {
if (typeof res.data !== 'undefined' && res.data.length > 0) {
// the array is defined and has at least one element
setIsTrainingExisting(true)
setTrainingData(res.data)
}
else {
setIsTrainingExisting(false)
}
}
})
.catch((error) => {
//console.log(error);
});
}
useEffect(() => {
getTrainingData();
}, []);
const deleteTraining = (id) => {
axios
.delete(`${process.env.REACT_APP_API_URL}/team/delete/team_training-${teamid}`,
{ data: { trainingsid: `${id}` } })
.then((res) => {
if (res.status === 200) {
var myArray = trainingData.filter(function (obj) {
return obj.trainingsid !== id;
});
//console.log(myArray)
setTrainingData(() => [...myArray]);
}
})
.catch((error) => {
console.log(error);
});
}
const addNewTraining = () => {
setAddTraining(true);
}
const addTrainingNew = () => {
axios
.post(`${process.env.REACT_APP_API_URL}/team/add/team_training-${teamid}`,
{ von: `${from}`, bis: `${until}`, tag: `${day}` })
.then((res) => {
if (res.status === 200) {
setAddTraining(false)
const newTraining = {
trainingsid: res.data,
mannschaftsid: teamid,
von: `${from}`,
bis: `${until}`,
tag: `${day}`
}
setTrainingData(() => [...trainingData, newTraining]);
//console.log(trainingData)
}
})
.catch((error) => {
console.log(error);
});
}
const [editing, setEditing] = useState(null);
const editingTraining = (id) => {
//console.log(id)
setEditing(id);
};
const updateTraining = (trainingsid) => {
}
return (
<div>
{trainingData.map((d, i) => (
<div key={i}>
Trainingszeiten
<input class="input is-normal" type="text" key={ d.trainingsid } value={day} placeholder="Wochentag" onChange={event => setDay(event.target.value)} readOnly={false}></input>
{d.tag} - {d.von} bis {d.bis} Uhr
<button className="button is-danger" onClick={() => deleteTraining(d.trainingsid)}>Lรถschen</button>
{editing === d.trainingsid ? (
<button className="button is-success" onClick={() => { editingTraining(null); updateTraining(d.trainingsid); }}>Save</button>
) : (
<button className="button is-info" onClick={() => editingTraining(d.trainingsid)}>Edit</button>
)}
<br />
</div>
))}
)
}
export default Training
The reason you see all fields changing is because when you build the input elements while using .map you are probably assigning the same onChange event and using the same state value to provide the value for the input element.
You should correctly manage this information and isolate the elements from their handlers. There are several ways to efficiently manage this with help of either useReducer or some other paradigm of your choice. I will provide a simple example showing the issue vs no issue with a controlled approach,
This is what I suspect you are doing, and this will show the issue. AS you can see, here I use the val to set the value of <input/> and that happens repeatedly for both the items for which we are building the elements,
const dataSource = [{id: '1', value: 'val1'}, {id: '2', value: 'val2'}]
export default function App() {
const [val, setVal]= useState('');
const onTextChange = (event) => {
setVal(event.target.value);
}
return (
<div className="App">
{dataSource.map(x => {
return (
<div key={x.id}>
<input type="text" value={val} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
This is how you would go about it.
export default function App() {
const [data, setData]= useState(dataSource);
const onTextChange = (event) => {
const id = String(event.target.dataset.id);
const val = String(event.target.value);
const match = data.find(x => x.id === id);
const updatedItem = {...match, value: val};
if(match && val){
const updatedArrayData = [...data.filter(x => x.id !== id), updatedItem];
const sortedData = updatedArrayData.sort((a, b) => Number(a.id) - Number(b.id));
console.log(sortedData);
setData(sortedData); // sorting to retain order of elements or else they will jump around
}
}
return (
<div className="App">
{data.map(x => {
return (
<div key={x.id}>
<input data-id={x.id} type="text" value={x.value} onChange={onTextChange}/>
</div>
)
})}
</div>
);
}
What im doing here is, finding a way to map an element to its own with the help of an identifier. I have used the data-id attribute for it. I use this value again in the callback to identify the match, update it correctly and update the state again so the re render shows correct values.

Resources