How to handle multiple inputs for the same variable in react? - reactjs

My colleague has been working on a small project and needs my help with it, I have barely any experience in react and have been trying to use my knowledge from other languages to handle this problem, however I have come to my limits after searching the web for hours and not finding an efficient way to handle this issue without having to rewrite most of the code.
So the problem is the following: We have customer orders with different variables, (i.e. Loading Country)
These are displayed in a table like this (we map over the data and print this tr/td for every order):
<td><input type="text" onChange={(e) => setLoadingCountry(e.target.value)} className={"lcountry"+data.rowid} value={data.array_options.options_lcountry}></input></td>
<td><input type="text" onChange={(e) => setLoadingZip(e.target.value)} className={"lpostcode"+data.rowid} value={data.array_options.options_lpostcode}></input></td>
setLoadingCountry is used for a useState.
With an update button for each row at the end:
<td><button className={data.id} onClick={(e) => handleCoData(e.target.className)}>Update</button></td>
handleCoData is a function I made, since we get the initial data from a json and I put the json into a useState array, this function handles that array after button click like the following:
const handleCoData = (coID) => {
setCoData(
coData.map((co) =>
co.id == coID
? {...co, array_options: {
options_lcountry: loadingCountry,
options_lpostcode: loadingZip
}}
: {...co}
)
);
console.log(coID);
console.log(coData);
};
Now: One Problem, which is the smaller one, is that we can only update one row at a time, since changing 2 countries from 2 rows without updating inbetween will overwrite the value of the first loading country in the useState.
The bigger problem is that, when you only want to change the loading country, and then click update, the Zip is either empty, because it was never set by the use state, or it has the value from another row which was changed previously. Is there any way to get the value from that specific row for that specific field?
If this question needs changes or additional information please tell me so, as I said I do not have much experience in React, and it is not my project either so I have no idea what might be needed to fully understand the issue.
Thank you.
Edit:
I was not able to make a codepen, since it would show a syntax error that I can't figure out quickly ( there isn't one in the actual code), but here is the code which includes all the things needed.
//import Data from "../../data.json";
const CombinedComponents = (apiKey) => {
const [loadingCountry, setLoadingCountry] = useState([]);
const [loadingZip, setLoadingZip] = useState([]);
const [coData, setCoData] = useState(Data);
console.log(coData);
const handleCoData = (coID) => {
setCoData(
coData.map((co) =>
co.id == coID
? {
...co,
array_options: {
options_lcountry: loadingCountry,
options_lpostcode: loadingZip
}
}
: { ...co }
)
);
console.log(coID);
console.log(coData);
};
return (
<div>
<table>
{visible && <TableHeadView1 />}
<tbody>
{coData.map((data) => (
<tr>
<td>{data.ref}</td>
<td>
<input
type="text"
onChange={(e) => setLoadingCountry(e.target.value)}
className={"lcountry" + data.rowid}
value={data.array_options.options_lcountry}
></input>
</td>
<td>
<input
type="text"
onChange={(e) => setLoadingZip(e.target.value)}
className={"lpostcode" + data.rowid}
value={data.array_options.options_lpostcode}
></input>
</td>
<td>
<button
className={data.id}
onClick={(e) => handleCoData(e.target.className)}
>
Update
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
};
This is what the array coData looks like (ofcourse with many more entries):
[{
"id": "11480",
"array_options": {
"options_lcountry": "1",
"options_lpostcode": "80190",
}
}]

Here your problem resides in using a single state value for handling multiple input values. If I was to do it, I'd only use the main array all the time, giving me multiple fields to change from. Currently, your update button only handles the React state, which is purely front end. You could use it to send the update to your backend instead.
My take on this one would be using these functions for inputs 'onChange' props:
const handleCountryUpdate = (id, value) => {
coData.find(it => it.id === id).array_options.options_lcountry = value;
setCoData([...coData]); // Using an array copy to override the state
};
const handlePostCodeUpdate = (id, value) => {
coData.find(it => it.id === id).array_options.options_lpostcode = value;
setCoData([...coData]);
};
...
<input type="text"
onChange={(e) => handlePostCodeUpdate(data.id, e.target.value)}
className={"lpostcode" + data.rowid}
value={data.array_options.options_lpostcode}>
</input>
Another way around would be to use a child component state to do the job
...
{coData.map((data) => <TableRow key={data.id}
data={data}
handleCoData={handleCoData}/>)}
...
const TableRow = props => {
const {data, handleCoData} = props;
const [loadingCountry, setLoadingCountry] = useState(data.array_options.options_lcountry);
const [loadingZip, setLoadingZip] = useState(data.array_options.options_lpostcode);
return (
<tr>
<td>{data.ref}</td>
<td>
<input
type="text"
onChange={(e) => setLoadingCountry(e.target.value)}
className={"lcountry" + data.rowid}
value={data.array_options.options_lcountry}
></input>
</td>
<td>
<input
type="text"
onChange={(e) => setLoadingZip(e.target.value)}
className={"lpostcode" + data.rowid}
value={data.array_options.options_lpostcode}
></input>
</td>
<td>
<button
className={data.id}
onClick={(e) => handleCoData(e.target.className)}
>
Update
</button>
</td>
</tr>
);
}

Related

Trying to create dynamic tables in React, getting bizarre error

So, I'm trying to create a simple program in react. The state is an array of objects. It prints these objects into a table. Standard table construction (, , ) etc. works fine for the initial render, but when I try to re-render I get bizarre errors I'm trying to adapt to. So, this is my code so far.
import React from "react";
import ReactDOM from "react-dom";
import { useState } from "react";
import "./style.css";
function Trains() {
const [trainsList, setTrainsList] = useState([
{ name: "Thomas", destination: "Boston" },
{ name: "Duncan", destination: "New York" },
]);
return (
<div>
<table>
<thead>name</thead>
<thead>destination</thead>
{trainsList.map((train) => (
<tbody key={train.name}>
<td>{train.name}</td>
<td>{train.destination}</td>
</tbody>
))}
</table>
<br />
Train Name: <input type="text" id="trainName" />
<br />
Destination: <input type="text" id="trainDest" />
<br />
<button
onClick={() =>
setTrainsList((prev) =>
prev.push({ name: "Dennis", destination: "Denville" })
)
}
>
Submit
</button>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Trains />);
But even changing the table lingo like that still isn't enough. I get these bizarre errors:
bundle.js:7294 Warning: validateDOMNesting(...): Text nodes cannot appear as a child of <thead>.
at thead
at table
at div
at Trains (http://localhost:3000/static/js/bundle.js:32:86)
And
bundle.js:7294 Warning: validateDOMNesting(...): <td> cannot appear as a child of <tbody>.
at td
at tbody
at table
at div
at Trains (http://localhost:3000/static/js/bundle.js:32:86)
How on Earth can I create a table or use "thead" to any effect if I can't even put any text in the headings or put tds in my table rows? How the heck do I make tables in react?
Table Rows
You've probably intended to make your table look something like that:
<table>
<thead>
<tr>
<th>name</th>
<th>destination</th>
</tr>
</thead>
<tbody>
{trainsList.map((train) => (
<tr key={index}>
<td>{train.name}</td>
<td>{train.destination}</td>
</tr>
))}
</tbody>
</table>
Note the <tr> elements to create a table rows both in the header and in the body.
Key in a map
Also, note the key attribute in the <tr> element. It is not a good practice to use an index of an array as a key, although it is much better that using a name that can be repeated.
Fix setState
Finally, the function to update state after clicking a button can be rewritten to that:
<button
onClick={() =>
setTrainsList((prev) => {
return [
...prev,
{ name: "Dennis", destination: "Denville" },
];
})
}
>
Submit
</button>
The error is in the setTrainsList method defined in the Submit button. The push method returns the new length of the array, but does not change the original array. Instead, you must use the concat method or the spread syntax to add a new element to an array in React:
<button onClick={() =>
setTrainsList((prev) => [...prev, { name: "Dennis", destination: "your butt" }])
}
>
Submit
</button>
This should resolve the error and add a new element to the train list when you click the button.

React input field mirrors the data from previous input

I am trying to build a small recipe app. One feature of the app is saving user recipes, including ingredient/qty/measurement.
I need to wrap up the inputted ingredients into an array of objects to send to server but right now my setIngredientList only works for the first two ingredients a user inputs.
When a user tries to add a third ingredient it just mirrors the data from the second input (and fills the third input's fields with the same data as the second input). It is like the second inputs and any subsequent input mirror each other.
I believe the problem is init is not clearing properly (it seems to clear after the first ingredient is added allowing the second one to be added, but then it does not clear for the next ingredients.
I'm not sure the proper way to make sure this happens so multiple ingredients can be added.
Here is my code:
const init = {
ingredient_name: '',
quantity: '',
measure: '',
}
export default function Recipe() {
const [name, setName] = useState('')
const [ingredientList, setIngredientList] = useState([
{
ingredient_name: '',
quantity: '',
measure: '',
},
])
const handleChange = (e, i) => {
const { name, value } = e.target
setIngredientList((prevState) => {
const newIngredientList = [...prevState]
newIngredientList[i][name] = value
return [...newIngredientList]
})
}
return (
<div>
<div className="recipe-form-container">
<form className="recipe-form">
[...]
</div>
{ingredientList.map((list, i) => (
<div key={i} className="ingredient-triad">
<input
className="ingredient"
name="ingredient_name"
type="text"
value={list.ingredient_name}
onChange={(e) => handleChange(e, i)}
></input>
<input
className="quantity"
name="quantity"
type="text"
value={list.quantity}
onChange={(e) => handleChange(e, i)}
></input>
<select
className="dropdown"
name="measure"
id="measure"
value={list.measure}
onChange={(e) => handleChange(e, i)}
>
<option value="" disabled>
--none--
</option>
<option value="cup">cup</option>
</select>
<button
onClick={(e) => {
console.log(init)
setIngredientList((prev) => [...prev, init])
e.preventDefault()
}}
>
Add
</button>
</div>
))}
</form>
</div>
</div>
)
}
Classical object reference issue.
Use the below code it will work fine.
Previously, you pass the same init object for multiple rows,
which is why you got that result. Instead of doing that, when the user clicks 'add' button then add a new Object to your state which is derived from your init object. Here I just clone the init object and then set the state.
<button
onClick={(e) => {
console.log(init);
setIngredientList((prev) => [...prev, { ...init }]);
e.preventDefault();
}}
>
Sounds like you wanted some advice and can prob find the solution yourself, but what i would do to this code:
Move e.preventDefault() above setIngredientList.
Create an Ingredient class with the state / logic within and pass state to Ingredient, the list should not not be concerned with an Ingredient, it just lists.
Then what is onClick doing - init - its the initial value isn't it, so I think that with your onClick you are overwriting prev with the value that is outside the component - should it be outside or is it some state - it holds a value - so it should be an initial state of an ingredient? Checkout react tools profiler so see exactly what is happening on your events.
So what is prev we are over writing it, but what is it? Isn't the first arg on onClick the click event? So I think you are overwriting the click event with the init state - which is outside the component - a few things to tweak for you (if you like), hope it helps.

How to do a single handler for array of inputs in React

I know how to make multiple handlers for multiple input fields but I would like to create just one handler working for dynamic number of input fields and I got stuck. I am unable to modify the inputs and they have fixed value.
This is the state of the main component
this.state = {
assetAccounts:["Cash","Financial Investment","Real Estate","Private Business Value"],
accounts:[0,1,2,3]
}
this.onChangeAccount = this.onChangeAccount.bind(this)
This is the handler
onChangeAccount = (e,index) => {
const value = e.target.value;
let data = this.state.accounts;
data[index]=value;
console.log(data)
this.setState({
accounts: [...data]
})
}
and this is rendered:
<table>
{this.state.assetAccounts.map((account,index)=>(<Account value={this.state.accounts[index]} name={account} key={index} onChange={this.onChangeAccount.bind(this)}/>))}
</table>
Account is my own component:
import React from 'react';
class Account extends React.Component {
render() {
return (
<div>
<tr>
<td width="200px">{this.props.name}</td>
<td>
<input
type="text" value={this.props.value}
className="form-control"
onChange={this.props.onChange}
/>
</td>
</tr>
</div>
);
}
}
export default Account;
If anyone knows how to make each of the individual accounts possible to edit separately, I would appreciate it a lot. Thanks for your help!
Prop onChange of component Account only accept one paramater e.
You should pass parameter index when use Account.
Just do like this:
<table>
{this.state.assetAccounts.map((account,index)=>(<Account value={this.state.accounts[index]} name={account} key={index} onChange={e => this.onChangeAccount(e, index)}/>))}
</table>
Actually, pass e.target.value as a parameter for onChange in Account instead of e is better.

react.js, how to create a search filter from API data?

I am trying to create my first search bar in React.js. I am trying to implement search functionality with filter method. I faced a problem with filter method, which gives an error like "filter is not defined". I am stuck on it for 2 days, I have looked several tutorials and endless youtube videos. This is the simpliest approach, I guess. Any help will be appreciated.
import React, { useState, useEffect } from "react";
import Recipe from "./Recipe";
import "./styles.css";
export default function RecipeList() {
const apiURL = "https://www.themealdb.com/api/json/v1/1/search.php?f=c";
const [myRecipes, setRecipes] = useState("");
const [search, setSearch] = useState("");
// fetch recipe from API
function fetchRecipes() {
fetch(apiURL)
.then(response => response.json())
.then(data => setRecipes(data.meals))
.catch(console.log("Error"));
}
function onDeleteHandler(index) {
setRecipes(
myRecipes.filter((element, filterIndex) => index !== filterIndex)
);
}
useEffect(() => {
fetchRecipes();
}, []);
const filterRecipes = myRecipe.meal.filter( element => {
return element.name.toLowerCase().includes(search.toLocaleLowerCase())
})
{/* filter method above doesn't work */}
return (
<div>
<label>
<div className="input-group mb-3 cb-search">
<input
type="text"
className="form-control"
placeholder="Search for recipes..."
aria-label="Recipient's username"
aria-describedby="button-addon2"
onChange = {e => setSearch (e.target.value)}
/>
<div className="input-group-append">
<button
className="btn btn-outline-secondary"
type="button"
id="button-addon2"
>
Search
</button>
</div>
</div>
</label>
<div>
<button
className="btn btn-info cb-button fetch-button"
onClick={fetchRecipes}
>
Fetch Recipe
</button>
<br />
{filterRecipes.map((element, index) => (
<Recipe
key={index}
index = {index}
onDelete={onDeleteHandler}
{...element}
name = {element.strMeal}
/>
))}
{/** name of child component */}
{/** strMeal is the name of Recipe in API object */}
</div>
</div>
);
}
link for code codesandbox
I made some changes on your code updated code
const [myRecipes, setRecipes] = useState([]);
You should declare myRecipes as an array if u intended to use map function.
const filterRecipes = myRecipe.meal.filter( element => {
return element.name.toLowerCase().includes(search.toLocaleLowerCase())
})
You have the wrong variable passing through, it should be myRecipes
filterRecipes.map((element, index) => (
<Recipe
key={index}
index = {index}
onDelete={onDeleteHandler}
{...element}
name = {element.strMeal}
/>
3. You should check whether your filterRecipes is not undefined before you use map function.
Lastly, your fetch API return error which unable to setRecipes.
I could not resolve you task completely because of low count of information according the task, but, i think, my answer will be useful for you.
So, tthe first thing I would like to draw attention to is a initial state in the parameter of useState function. In this task it sould be as:
const [myRecipes, setRecipes] = useState({meals: []});
Because, before fetching data, React has a time to run the code, and, when it come to line 32, it see what in the myRecipes (myRecipes, not a myRecipe. Please, pay attention when you write the code) a string except an array.
And in the line 32 i recommend you to add something checking of have you resolved request of data like:
const filterRecipes = myRecipes.meals.length
? myRecipes.meals.filter(element => {
return element.name.toLowerCase().includes(search.toLocaleLowerCase());
});
: []
And look in the data which you receive, because, i think, there are no elements with propName like name (element.name).
I think, i could help you as possible. If you have any questions, ask in comments. Will answer you as soon as possible. Good luck

React - How to set the value of many input controls that is built dynamically?

I'm building a component dynamically with many inputs, and I need to handle the onChange on these inputs...
So what I tired is to set a name for each input control and then on onChange call to set a state variable with the name of that control, and later on to use it from state to fill the "value" property of the input control :
else if (element.Type === TEXT) {
elements.push(
<td className="view-cell" align="left">
{element.Name}<br/>
<Input key={element.Id}
name={element.Name.toLowerCase().replace(" ","")}
// label={element.Name}
// defaultValue={element.Value}
value={this.state.valueOf(element.Name.toLowerCase().replace(" ",""))}
disabled={!element.Editable}
onChange={(e) => {
this.onInputValueChange(e)
}}
/>
</td>
)
}
onInputValueChange(e) {
this.setState({[e.target.name]: e.target.value})
console.log(this.state.valueOf(e.target.name))
}
But I can see that the state is not updated with the new values that I'm trying to add to it dynamically by the inputs names!
How can I achieve it?
Update
When I try to access the state value from console I'm getting:
I noticed two things on your code:
You are trying to get the values from valueOf state variable. But you're missing to set the values in valueOf object in your onchange handler. If you add the missing bits, it should be fine.
onInputValueChange(e) {
this.setState({
[e.target.name]: e.target.value
}, () => {
console.log(this.state[e.target.name]);
// use [] to access value instead of ()
});
}
Also, you have to access the values dynamically from an object by using square bracket [] notation.
if (element.Type === TEXT) {
const inputName = element.Name.toLowerCase().replace(" ","");
elements.push(
<td className="view-cell" align="left">
{element.Name}<br/>
<Input
key={element.Id}
name={inputName}
label={element.Name}
defaultValue={element.Value}
value={this.state[inputName]}
disabled={!element.Editable}
onChange={this.onInputValueChange}
/>
)
}
This should fix your issue.

Resources